Skip to content

Commit cb89deb

Browse files
committed
refactor: add transaction support (facebook#12)
1 parent 38aa8e4 commit cb89deb

File tree

5 files changed

+127
-23
lines changed

5 files changed

+127
-23
lines changed

src/NavigationContainer.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ const MISSING_CONTEXT_ERROR =
1818
export const NavigationStateContext = React.createContext<{
1919
state?: NavigationState | PartialState;
2020
getState: () => NavigationState | PartialState | undefined;
21-
setState: (state: NavigationState | undefined) => void;
21+
setState: (state: NavigationState | undefined, dangerously?: boolean) => void;
2222
key?: string;
23+
performTransaction: (action: () => void) => void;
2324
}>({
2425
get getState(): any {
2526
throw new Error(MISSING_CONTEXT_ERROR);
2627
},
2728
get setState(): any {
2829
throw new Error(MISSING_CONTEXT_ERROR);
2930
},
31+
get performTransaction(): any {
32+
throw new Error(MISSING_CONTEXT_ERROR);
33+
},
3034
});
3135

3236
export default class NavigationContainer extends React.Component<Props, State> {
@@ -42,10 +46,39 @@ export default class NavigationContainer extends React.Component<Props, State> {
4246
}
4347
}
4448

45-
private getNavigationState = () => this.state.navigationState;
49+
private navigationState:
50+
| NavigationState
51+
| PartialState
52+
| undefined
53+
| null = null;
54+
55+
private performTransaction = (action: () => void) => {
56+
this.setState(
57+
state => {
58+
this.navigationState = state.navigationState;
59+
action();
60+
return { navigationState: this.navigationState };
61+
},
62+
() => (this.navigationState = null)
63+
);
64+
};
4665

47-
private setNavigationState = (navigationState: NavigationState | undefined) =>
48-
this.setState({ navigationState });
66+
private getNavigationState = () =>
67+
this.navigationState || this.state.navigationState;
68+
69+
private setNavigationState = (
70+
navigationState: NavigationState | undefined,
71+
dangerously = false
72+
) => {
73+
if (this.navigationState === null && !dangerously) {
74+
throw new Error('setState need to be wrapped in a performTransaction');
75+
}
76+
if (dangerously) {
77+
this.setState({ navigationState });
78+
} else {
79+
this.navigationState = navigationState;
80+
}
81+
};
4982

5083
render() {
5184
return (
@@ -54,6 +87,7 @@ export default class NavigationContainer extends React.Component<Props, State> {
5487
state: this.state.navigationState,
5588
getState: this.getNavigationState,
5689
setState: this.setNavigationState,
90+
performTransaction: this.performTransaction,
5791
}}
5892
>
5993
<EnsureSingleNavigator>{this.props.children}</EnsureSingleNavigator>

src/SceneView.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,27 @@ type Props = {
1919

2020
export default function SceneView(props: Props) {
2121
const { screen, route, navigation: helpers, getState, setState } = props;
22+
const { performTransaction } = React.useContext(NavigationStateContext);
2223

2324
const navigation = React.useMemo(
2425
() => ({
2526
...helpers,
2627
setParams: (params: object) => {
27-
const state = getState();
28+
performTransaction(() => {
29+
const state = getState();
2830

29-
setState({
30-
...state,
31-
routes: state.routes.map(r =>
32-
r.key === route.key
33-
? { ...r, params: { ...r.params, ...params } }
34-
: r
35-
),
31+
setState({
32+
...state,
33+
routes: state.routes.map(r =>
34+
r.key === route.key
35+
? { ...r, params: { ...r.params, ...params } }
36+
: r
37+
),
38+
});
3639
});
3740
},
3841
}),
39-
[getState, helpers, route.key, setState]
42+
[getState, helpers, performTransaction, route.key, setState]
4043
);
4144

4245
const getCurrentState = React.useCallback(() => {
@@ -65,9 +68,16 @@ export default function SceneView(props: Props) {
6568
state: route.state,
6669
getState: getCurrentState,
6770
setState: setCurrentState,
71+
performTransaction,
6872
key: route.key,
6973
}),
70-
[getCurrentState, route.key, route.state, setCurrentState]
74+
[
75+
getCurrentState,
76+
performTransaction,
77+
route.key,
78+
route.state,
79+
setCurrentState,
80+
]
7181
);
7282

7383
return (

src/__tests__/NavigationContainer.test.tsx

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22
import { render } from 'react-native-testing-library';
3-
import { NavigationStateContext } from '../NavigationContainer';
3+
import NavigationContainer, {
4+
NavigationStateContext,
5+
} from '../NavigationContainer';
46

57
it('throws when getState is accessed without a container', () => {
68
expect.assertions(1);
@@ -39,3 +41,46 @@ it('throws when setState is accessed without a container', () => {
3941
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
4042
);
4143
});
44+
45+
it('throws when performTransaction is accessed without a container', () => {
46+
expect.assertions(1);
47+
48+
const Test = () => {
49+
const { performTransaction } = React.useContext(NavigationStateContext);
50+
51+
// eslint-disable-next-line babel/no-unused-expressions
52+
performTransaction;
53+
54+
return null;
55+
};
56+
57+
const element = <Test />;
58+
59+
expect(() => render(element).update(element)).toThrowError(
60+
"We couldn't find a navigation context. Have you wrapped your app with 'NavigationContainer'?"
61+
);
62+
});
63+
64+
it('throws when setState is called outside performTransaction', () => {
65+
expect.assertions(1);
66+
67+
const Test = () => {
68+
const { setState } = React.useContext(NavigationStateContext);
69+
React.useEffect(() => {
70+
setState(undefined);
71+
// eslint-disable-next-line react-hooks/exhaustive-deps
72+
}, []);
73+
74+
return null;
75+
};
76+
77+
const element = (
78+
<NavigationContainer>
79+
<Test />
80+
</NavigationContainer>
81+
);
82+
83+
expect(() => render(element).update(element)).toThrowError(
84+
'setState need to be wrapped in a performTransaction'
85+
);
86+
});

src/useNavigationBuilder.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export default function useNavigationBuilder(
7474
getState: getCurrentState,
7575
setState,
7676
key,
77+
performTransaction,
7778
} = React.useContext(NavigationStateContext);
7879

7980
let state = router.getRehydratedState({
@@ -93,7 +94,7 @@ export default function useNavigationBuilder(
9394
// If the state needs to be updated, we'll schedule an update with React
9495
// setState in render seems hacky, but that's how React docs implement getDerivedPropsFromState
9596
// https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops
96-
setState(nextState);
97+
setState(nextState, true);
9798
}
9899

99100
state = nextState;
@@ -102,7 +103,9 @@ export default function useNavigationBuilder(
102103
React.useEffect(() => {
103104
return () => {
104105
// We need to clean up state for this navigator on unmount
105-
getCurrentState() !== undefined && setState(undefined);
106+
performTransaction(
107+
() => getCurrentState() !== undefined && setState(undefined)
108+
);
106109
};
107110
// eslint-disable-next-line react-hooks/exhaustive-deps
108111
}, []);

src/useNavigationHelpers.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
NavigationState,
88
ActionCreators,
99
} from './types';
10+
import { NavigationStateContext } from './NavigationContainer';
1011

1112
type Options = {
1213
onAction: (action: NavigationAction, sourceNavigatorKey?: string) => boolean;
@@ -25,15 +26,19 @@ export default function useNavigationHelpers({
2526
NavigationBuilderContext
2627
);
2728

29+
const { performTransaction } = React.useContext(NavigationStateContext);
30+
2831
return React.useMemo((): NavigationHelpers => {
2932
const dispatch = (
3033
action: NavigationAction | ((state: NavigationState) => NavigationState)
3134
) => {
32-
if (typeof action === 'function') {
33-
setState(action(getState()));
34-
} else {
35-
onAction(action);
36-
}
35+
performTransaction(() => {
36+
if (typeof action === 'function') {
37+
setState(action(getState()));
38+
} else {
39+
onAction(action);
40+
}
41+
});
3742
};
3843

3944
const actions = {
@@ -54,5 +59,12 @@ export default function useNavigationHelpers({
5459
),
5560
dispatch,
5661
};
57-
}, [getState, onAction, parentNavigationHelpers, actionCreators, setState]);
62+
}, [
63+
actionCreators,
64+
parentNavigationHelpers,
65+
performTransaction,
66+
setState,
67+
getState,
68+
onAction,
69+
]);
5870
}

0 commit comments

Comments
 (0)