Skip to content

Commit 7cf33d8

Browse files
Merge pull request #237 from cleandart/CPLAT-8036-useContext-hook
CPLAT-8036 Add useContext Hook
2 parents d65d845 + d1b6314 commit 7cf33d8

File tree

5 files changed

+188
-14
lines changed

5 files changed

+188
-14
lines changed

example/test/function_component_test.dart

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@ UseStateTestComponent(Map props) {
1212

1313
return react.div({}, [
1414
count.value,
15-
react.button({'onClick': (_) => count.set(0)}, ['Reset']),
16-
react.button({
17-
'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
18-
}, [
19-
'+'
20-
]),
15+
react.button({'onClick': (_) => count.set(0), 'key': 'ust1'}, ['Reset']),
16+
react.button({'onClick': (_) => count.setWithUpdater((prev) => prev + 1), 'key': 'ust2'}, ['+']),
2117
]);
2218
}
2319

@@ -37,29 +33,96 @@ UseCallbackTestComponent(Map props) {
3733
}, []);
3834

3935
return react.div({}, [
40-
react.div({}, ['Delta is ${delta.value}']),
41-
react.div({}, ['Count is ${count.value}']),
42-
react.button({'onClick': increment}, ['Increment count']),
43-
react.button({'onClick': incrementDelta}, ['Increment delta']),
36+
react.div({'key': 'ucbt1'}, ['Delta is ${delta.value}']),
37+
react.div({'key': 'ucbt2'}, ['Count is ${count.value}']),
38+
react.button({'onClick': increment, 'key': 'ucbt3'}, ['Increment count']),
39+
react.button({'onClick': incrementDelta, 'key': 'ucbt4'}, ['Increment delta']),
40+
]);
41+
}
42+
43+
var useContextTestFunctionComponent =
44+
react.registerFunctionComponent(UseContextTestComponent, displayName: 'useContextTest');
45+
46+
UseContextTestComponent(Map props) {
47+
final context = useContext(TestNewContext);
48+
return react.div({
49+
'key': 'uct1'
50+
}, [
51+
react.div({'key': 'uct2'}, ['useContext counter value is ${context['renderCount']}']),
4452
]);
4553
}
4654

55+
int calculateChangedBits(currentValue, nextValue) {
56+
int result = 1 << 1;
57+
if (nextValue['renderCount'] % 2 == 0) {
58+
result |= 1 << 2;
59+
}
60+
return result;
61+
}
62+
63+
var TestNewContext = react.createContext<Map>({'renderCount': 0}, calculateChangedBits);
64+
65+
var newContextProviderComponent = react.registerComponent(() => _NewContextProviderComponent());
66+
67+
class _NewContextProviderComponent extends react.Component2 {
68+
get initialState => {'renderCount': 0, 'complexMap': false};
69+
70+
render() {
71+
final provideMap = {'renderCount': this.state['renderCount']};
72+
73+
return react.div({
74+
'key': 'ulasda',
75+
'style': {
76+
'marginTop': 20,
77+
}
78+
}, [
79+
react.button({
80+
'type': 'button',
81+
'key': 'button',
82+
'className': 'btn btn-primary',
83+
'onClick': _onButtonClick,
84+
}, 'Redraw'),
85+
react.br({'key': 'break1'}),
86+
'TestContext.Provider props.value: ${provideMap}',
87+
react.br({'key': 'break2'}),
88+
react.br({'key': 'break3'}),
89+
TestNewContext.Provider(
90+
{'key': 'tcp', 'value': provideMap},
91+
props['children'],
92+
),
93+
]);
94+
}
95+
96+
_onButtonClick(event) {
97+
this.setState({'renderCount': this.state['renderCount'] + 1, 'complexMap': false});
98+
}
99+
}
100+
47101
void main() {
48102
setClientConfiguration();
49103

50104
render() {
51105
react_dom.render(
52-
react.Fragment({}, [
106+
react.Fragment({
107+
'key': 'fctf'
108+
}, [
53109
react.h1({'key': 'functionComponentTestLabel'}, ['Function Component Tests']),
54110
react.h2({'key': 'useStateTestLabel'}, ['useState Hook Test']),
55111
useStateTestFunctionComponent({
56112
'key': 'useStateTest',
57113
}, []),
58-
react.br({}),
114+
react.br({'key': 'br'}),
59115
react.h2({'key': 'useCallbackTestLabel'}, ['useCallback Hook Test']),
60116
useCallbackTestFunctionComponent({
61117
'key': 'useCallbackTest',
62118
}, []),
119+
newContextProviderComponent({
120+
'key': 'provider'
121+
}, [
122+
useContextTestFunctionComponent({
123+
'key': 'useContextTest',
124+
}, []),
125+
]),
63126
]),
64127
querySelector('#content'));
65128
}

lib/hooks.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,31 @@ StateHook<T> useStateLazy<T>(T init()) => StateHook.lazy(init);
135135
///
136136
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usecallback>.
137137
Function useCallback(Function callback, List dependencies) => React.useCallback(allowInterop(callback), dependencies);
138+
139+
/// Returns the value of the nearest [Context.Provider] for the provided [context] object every time that context is
140+
/// updated.
141+
///
142+
/// The usage is similar to that of a [Context.Consumer] in that the return type of [useContext] is dependent upon
143+
/// the typing of the value passed into [createContext] and [Context.Provider].
144+
///
145+
/// > __Note:__ there are two [rules for using Hooks](https://reactjs.org/docs/hooks-rules.html):
146+
/// >
147+
/// > * Only call Hooks at the top level.
148+
/// > * Only call Hooks from inside a [DartFunctionComponent].
149+
///
150+
/// __Example__:
151+
///
152+
/// ```
153+
/// Context countContext = createContext(0);
154+
///
155+
/// UseCallbackTestComponent(Map props) {
156+
/// final count = useContext(countContext);
157+
///
158+
/// return react.div({}, [
159+
/// react.div({}, ['The count from context is $count']), // initially renders: 'The count from context is 0'
160+
/// ]);
161+
/// }
162+
/// ```
163+
///
164+
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usecontext>.
165+
T useContext<T>(Context<T> context) => ContextHelpers.unjsifyNewContext(React.useContext(context.jsThis));

lib/react_client/react_interop.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ abstract class React {
4848

4949
external static List<dynamic> useState(dynamic value);
5050
external static Function useCallback(Function callback, List dependencies);
51+
external static ReactContext useContext(ReactContext context);
5152
}
5253

5354
/// Creates a [Ref] object that can be attached to a [ReactElement] via the ref prop.

lib/src/context.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import 'package:react/react_client.dart';
4747
/// }
4848
///
4949
/// Learn more at: https://reactjs.org/docs/context.html
50-
class Context {
50+
class Context<T> {
5151
Context(this.Provider, this.Consumer, this._jsThis);
5252
final ReactContext _jsThis;
5353

@@ -101,7 +101,7 @@ class Context {
101101
/// }
102102
///
103103
/// Learn more: https://reactjs.org/docs/context.html#reactcreatecontext
104-
Context createContext<TValue>([
104+
Context<TValue> createContext<TValue>([
105105
TValue defaultValue,
106106
int Function(TValue currentValue, TValue nextValue) calculateChangedBits,
107107
]) {

test/hooks_test.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'dart:html';
88
import "package:js/js.dart";
99
import 'package:react/hooks.dart';
1010
import 'package:react/react.dart' as react;
11+
import 'package:react/react.dart';
1112
import 'package:react/react_client.dart';
1213
import 'package:react/react_dom.dart' as react_dom;
1314
import 'package:react/react_test_utils.dart' as react_test_utils;
@@ -196,5 +197,86 @@ main() {
196197
});
197198
});
198199
});
200+
201+
group('useContext -', () {
202+
DivElement mountNode;
203+
_ContextProviderWrapper providerRef;
204+
int currentCount = 0;
205+
Context<int> testContext;
206+
Function useContextTestFunctionComponent;
207+
208+
setUp(() {
209+
mountNode = DivElement();
210+
211+
UseContextTestComponent(Map props) {
212+
final context = useContext(testContext);
213+
currentCount = context;
214+
return react.div({
215+
'key': 'uct1'
216+
}, [
217+
react.div({'key': 'uct2'}, ['useContext counter value is ${context}']),
218+
]);
219+
}
220+
221+
testContext = react.createContext(1);
222+
useContextTestFunctionComponent =
223+
react.registerFunctionComponent(UseContextTestComponent, displayName: 'useContextTest');
224+
225+
react_dom.render(
226+
ContextProviderWrapper({
227+
'contextToUse': testContext,
228+
'mode': 'increment',
229+
'ref': (ref) {
230+
providerRef = ref;
231+
}
232+
}, [
233+
useContextTestFunctionComponent({'key': 't1'}, []),
234+
]),
235+
mountNode);
236+
});
237+
238+
tearDown(() {
239+
react_dom.unmountComponentAtNode(mountNode);
240+
mountNode = null;
241+
currentCount = 0;
242+
testContext = null;
243+
useContextTestFunctionComponent = null;
244+
providerRef = null;
245+
});
246+
247+
group('updates with the correct values', () {
248+
test('on first render', () {
249+
expect(currentCount, 1);
250+
});
251+
252+
test('on value updates', () {
253+
providerRef.increment();
254+
expect(currentCount, 2);
255+
providerRef.increment();
256+
expect(currentCount, 3);
257+
providerRef.increment();
258+
expect(currentCount, 4);
259+
});
260+
});
261+
});
199262
});
200263
}
264+
265+
ReactDartComponentFactoryProxy2 ContextProviderWrapper = react.registerComponent(() => new _ContextProviderWrapper());
266+
267+
class _ContextProviderWrapper extends react.Component2 {
268+
get initialState {
269+
return {'counter': 1};
270+
}
271+
272+
increment() {
273+
this.setState({'counter': state['counter'] + 1});
274+
}
275+
276+
render() {
277+
return react.div({}, [
278+
props['contextToUse']
279+
.Provider({'value': props['mode'] == 'increment' ? state['counter'] : props['value']}, props['children'])
280+
]);
281+
}
282+
}

0 commit comments

Comments
 (0)