Skip to content

Commit f096d6b

Browse files
authored
Merge pull request #28 from josefdolezal/feature/lazy-initial-state
Lazy initial state using closures in useState hook
2 parents bd7c60f + 2d01397 commit f096d6b

File tree

4 files changed

+89
-5
lines changed

4 files changed

+89
-5
lines changed

README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,11 @@ And then, include "Hooks" as a dependency for your target:
117117

118118
```swift
119119
func useState<State>(_ initialState: State) -> Binding<State>
120+
func useState<State>(_ initialState: @escaping () -> State) -> Binding<State>
120121
```
121122

122123
A hook to use a `Binding<State>` wrapping current state to be updated by setting a new state to `wrappedValue`.
123-
Triggers a view update when the state has been changed.
124+
Triggers a view update when the state has been changed.
124125

125126
```swift
126127
let count = useState(0) // Binding<Int>
@@ -130,6 +131,20 @@ Button("Increment") {
130131
}
131132
```
132133

134+
If the initial state is the result of an expensive computation, you may provide a closure instead.
135+
The closure will be executed once, during the initial render.
136+
137+
```swift
138+
let count = useState {
139+
let initialState = expensiveComputation() // Int
140+
return initialState
141+
} // Binding<Int>
142+
143+
Button("Increment") {
144+
count.wrappedValue += 1
145+
}
146+
```
147+
133148
</details>
134149

135150
<details>

Sources/Hooks/Hook/UseState.swift

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
import SwiftUI
22

3+
/// A hook to use a `Binding<State>` wrapping current state to be updated by setting a new state to `wrappedValue`.
4+
/// Triggers a view update when the state has been changed.
5+
///
6+
/// let count = useState {
7+
/// let initialState = expensiveComputation() // Int
8+
/// return initialState
9+
/// } // Binding<Int>
10+
///
11+
/// Button("Increment") {
12+
/// count.wrappedValue += 1
13+
/// }
14+
///
15+
/// - Parameter initialState: A closure creating an initial state. The closure will only be called once, during the initial render.
16+
/// - Returns: A `Binding<State>` wrapping current state.
17+
public func useState<State>(_ initialState: @escaping () -> State) -> Binding<State> {
18+
useHook(StateHook(initialState: initialState))
19+
}
20+
321
/// A hook to use a `Binding<State>` wrapping current state to be updated by setting a new state to `wrappedValue`.
422
/// Triggers a view update when the state has been changed.
523
///
@@ -12,15 +30,17 @@ import SwiftUI
1230
/// - Parameter initialState: An initial state.
1331
/// - Returns: A `Binding<State>` wrapping current state.
1432
public func useState<State>(_ initialState: State) -> Binding<State> {
15-
useHook(StateHook(initialState: initialState))
33+
useState {
34+
initialState
35+
}
1636
}
1737

1838
private struct StateHook<State>: Hook {
19-
let initialState: State
39+
let initialState: () -> State
2040
var updateStrategy: HookUpdateStrategy? = .once
2141

2242
func makeState() -> Ref {
23-
Ref(initialState: initialState)
43+
Ref(initialState: initialState())
2444
}
2545

2646
func value(coordinator: Coordinator) -> Binding<State> {

Sources/Hooks/Hooks.docc/Hooks.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ Furthermore, hooks such as useEffect also solve the problem of lack of lifecycle
1616

1717
### Hooks
1818

19-
- ``useState(_:)``
19+
- ``useState(_:)-52rjz``
20+
- ``useState(_:)-jg02``
2021
- ``useEffect(_:_:)``
2122
- ``useLayoutEffect(_:_:)``
2223
- ``useMemo(_:_:)``

Tests/HooksTests/Hook/UseStateTests.swift

+48
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,54 @@ final class UseStateTests: XCTestCase {
4141
XCTAssertEqual(tester.value.wrappedValue, 0)
4242
}
4343

44+
func testInitialStateCreatedOnEachUpdate() {
45+
var updateCalls = 0
46+
47+
func createState() -> Int {
48+
updateCalls += 1
49+
return 0
50+
}
51+
52+
let tester = HookTester {
53+
useState(createState())
54+
}
55+
56+
XCTAssertEqual(updateCalls, 1)
57+
58+
tester.update()
59+
60+
XCTAssertEqual(updateCalls, 2)
61+
62+
tester.update()
63+
64+
XCTAssertEqual(updateCalls, 3)
65+
}
66+
67+
func testInitialStateCreateOnceWhenGivenClosure() {
68+
var closureCalls = 0
69+
70+
func createState() -> Int {
71+
closureCalls += 1
72+
return 0
73+
}
74+
75+
let tester = HookTester {
76+
useState {
77+
createState()
78+
}
79+
}
80+
81+
XCTAssertEqual(closureCalls, 1)
82+
83+
tester.update()
84+
85+
XCTAssertEqual(closureCalls, 1)
86+
87+
tester.update()
88+
89+
XCTAssertEqual(closureCalls, 1)
90+
}
91+
4492
func testDispose() {
4593
let tester = HookTester {
4694
useState(0)

0 commit comments

Comments
 (0)