Skip to content

CPLAT-8036 Add useContext Hook #237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 75 additions & 12 deletions example/test/function_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@ UseStateTestComponent(Map props) {

return react.div({}, [
count.value,
react.button({'onClick': (_) => count.set(0)}, ['Reset']),
react.button({
'onClick': (_) => count.setWithUpdater((prev) => prev + 1),
}, [
'+'
]),
react.button({'onClick': (_) => count.set(0), 'key': 'ust1'}, ['Reset']),
react.button({'onClick': (_) => count.setWithUpdater((prev) => prev + 1), 'key': 'ust2'}, ['+']),
]);
}

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

return react.div({}, [
react.div({}, ['Delta is ${delta.value}']),
react.div({}, ['Count is ${count.value}']),
react.button({'onClick': increment}, ['Increment count']),
react.button({'onClick': incrementDelta}, ['Increment delta']),
react.div({'key': 'ucbt1'}, ['Delta is ${delta.value}']),
react.div({'key': 'ucbt2'}, ['Count is ${count.value}']),
react.button({'onClick': increment, 'key': 'ucbt3'}, ['Increment count']),
react.button({'onClick': incrementDelta, 'key': 'ucbt4'}, ['Increment delta']),
]);
}

var useContextTestFunctionComponent =
react.registerFunctionComponent(UseContextTestComponent, displayName: 'useContextTest');

UseContextTestComponent(Map props) {
final context = useContext(TestNewContext);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It bothers me a bit that context will be dynamic unless you provide a type on the left side, and even then - there'd be no way to ensure that the type doesn't mismatch with the underlying type that was returned by createContext. Makes me wonder what the point of the generic parameter is on createContext.

By making these changes locally... all the typing seems to line up and makes context correctly inferred as a Map in this situation as a result of createContext<Map> being called on line 63.

// context.dart

- class Context {
+ class Context<T> {
// ...
}

- Context createContext<TValue>([
+ Context<TValue> createContext<TValue>([
// ...
// hooks.dart

- dynamic useContext(Context context) => // ...
+ T useContext<T>(Context<T> context) => // ...

@greglittlefield-wf @kealjones-wk @joebingham-wk is there some reason that I forgot about that led us to omit generic typing from the Context class?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I don't recall, I feel like there would have been a reason because the Context object in OverReact is typed exactly like that and i don't know why i would have omitted it in React-Dart unless it was just a minor oversight (very likely). @joebingham-wk if you can add this typing id say do it, but test the local context examples in browser and test it with over_react (tests & real examples if already in place) to make sure it doesn't break anything (i don't think it should).

return react.div({
'key': 'uct1'
}, [
react.div({'key': 'uct2'}, ['useContext counter value is ${context['renderCount']}']),
]);
}

int calculateChangedBits(currentValue, nextValue) {
int result = 1 << 1;
if (nextValue['renderCount'] % 2 == 0) {
result |= 1 << 2;
}
return result;
}

var TestNewContext = react.createContext<Map>({'renderCount': 0}, calculateChangedBits);

var newContextProviderComponent = react.registerComponent(() => _NewContextProviderComponent());

class _NewContextProviderComponent extends react.Component2 {
get initialState => {'renderCount': 0, 'complexMap': false};

render() {
final provideMap = {'renderCount': this.state['renderCount']};

return react.div({
'key': 'ulasda',
'style': {
'marginTop': 20,
}
}, [
react.button({
'type': 'button',
'key': 'button',
'className': 'btn btn-primary',
'onClick': _onButtonClick,
}, 'Redraw'),
react.br({'key': 'break1'}),
'TestContext.Provider props.value: ${provideMap}',
react.br({'key': 'break2'}),
react.br({'key': 'break3'}),
TestNewContext.Provider(
{'key': 'tcp', 'value': provideMap},
props['children'],
),
]);
}

_onButtonClick(event) {
this.setState({'renderCount': this.state['renderCount'] + 1, 'complexMap': false});
}
}

void main() {
setClientConfiguration();

render() {
react_dom.render(
react.Fragment({}, [
react.Fragment({
'key': 'fctf'
}, [
react.h1({'key': 'functionComponentTestLabel'}, ['Function Component Tests']),
react.h2({'key': 'useStateTestLabel'}, ['useState Hook Test']),
useStateTestFunctionComponent({
'key': 'useStateTest',
}, []),
react.br({}),
react.br({'key': 'br'}),
react.h2({'key': 'useCallbackTestLabel'}, ['useCallback Hook Test']),
useCallbackTestFunctionComponent({
'key': 'useCallbackTest',
}, []),
newContextProviderComponent({
'key': 'provider'
}, [
useContextTestFunctionComponent({
'key': 'useContextTest',
}, []),
]),
]),
querySelector('#content'));
}
Expand Down
28 changes: 28 additions & 0 deletions lib/hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,31 @@ StateHook<T> useStateLazy<T>(T init()) => StateHook.lazy(init);
///
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usecallback>.
Function useCallback(Function callback, List dependencies) => React.useCallback(allowInterop(callback), dependencies);

/// Returns the value of the nearest [Context.Provider] for the provided [context] object every time that context is
/// updated.
///
/// The usage is similar to that of a [Context.Consumer] in that the return type of [useContext] is dependent upon
/// the typing of the value passed into [createContext] and [Context.Provider].
///
/// > __Note:__ there are two [rules for using Hooks](https://reactjs.org/docs/hooks-rules.html):
/// >
/// > * Only call Hooks at the top level.
/// > * Only call Hooks from inside a [DartFunctionComponent].
///
/// __Example__:
///
/// ```
/// Context countContext = createContext(0);
///
/// UseCallbackTestComponent(Map props) {
/// final count = useContext(countContext);
///
/// return react.div({}, [
/// react.div({}, ['The count from context is $count']), // initially renders: 'The count from context is 0'
/// ]);
/// }
/// ```
///
/// Learn more: <https://reactjs.org/docs/hooks-reference.html#usecontext>.
T useContext<T>(Context<T> context) => ContextHelpers.unjsifyNewContext(React.useContext(context.jsThis));
1 change: 1 addition & 0 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ abstract class React {

external static List<dynamic> useState(dynamic value);
external static Function useCallback(Function callback, List dependencies);
external static ReactContext useContext(ReactContext context);
}

/// Creates a [Ref] object that can be attached to a [ReactElement] via the ref prop.
Expand Down
4 changes: 2 additions & 2 deletions lib/src/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import 'package:react/react_client.dart';
/// }
///
/// Learn more at: https://reactjs.org/docs/context.html
class Context {
class Context<T> {
Context(this.Provider, this.Consumer, this._jsThis);
final ReactContext _jsThis;

Expand Down Expand Up @@ -101,7 +101,7 @@ class Context {
/// }
///
/// Learn more: https://reactjs.org/docs/context.html#reactcreatecontext
Context createContext<TValue>([
Context<TValue> createContext<TValue>([
TValue defaultValue,
int Function(TValue currentValue, TValue nextValue) calculateChangedBits,
]) {
Expand Down
82 changes: 82 additions & 0 deletions test/hooks_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'dart:html';
import "package:js/js.dart";
import 'package:react/hooks.dart';
import 'package:react/react.dart' as react;
import 'package:react/react.dart';
import 'package:react/react_client.dart';
import 'package:react/react_dom.dart' as react_dom;
import 'package:react/react_test_utils.dart' as react_test_utils;
Expand Down Expand Up @@ -196,5 +197,86 @@ main() {
});
});
});

group('useContext -', () {
DivElement mountNode;
_ContextProviderWrapper providerRef;
int currentCount = 0;
Context<int> testContext;
Function useContextTestFunctionComponent;

setUp(() {
mountNode = DivElement();

UseContextTestComponent(Map props) {
final context = useContext(testContext);
currentCount = context;
return react.div({
'key': 'uct1'
}, [
react.div({'key': 'uct2'}, ['useContext counter value is ${context}']),
]);
}

testContext = react.createContext(1);
useContextTestFunctionComponent =
react.registerFunctionComponent(UseContextTestComponent, displayName: 'useContextTest');

react_dom.render(
ContextProviderWrapper({
'contextToUse': testContext,
'mode': 'increment',
'ref': (ref) {
providerRef = ref;
}
}, [
useContextTestFunctionComponent({'key': 't1'}, []),
]),
mountNode);
});

tearDown(() {
react_dom.unmountComponentAtNode(mountNode);
mountNode = null;
currentCount = 0;
testContext = null;
useContextTestFunctionComponent = null;
providerRef = null;
});

group('updates with the correct values', () {
test('on first render', () {
expect(currentCount, 1);
});

test('on value updates', () {
providerRef.increment();
expect(currentCount, 2);
providerRef.increment();
expect(currentCount, 3);
providerRef.increment();
expect(currentCount, 4);
});
});
});
});
}

ReactDartComponentFactoryProxy2 ContextProviderWrapper = react.registerComponent(() => new _ContextProviderWrapper());

class _ContextProviderWrapper extends react.Component2 {
get initialState {
return {'counter': 1};
}

increment() {
this.setState({'counter': state['counter'] + 1});
}

render() {
return react.div({}, [
props['contextToUse']
.Provider({'value': props['mode'] == 'increment' ? state['counter'] : props['value']}, props['children'])
]);
}
}