id | title | sidebar_label |
---|---|---|
recipes |
Recipes |
Recipes |
React Tracked provides a primitive API, and there are various ways to use it for apps.
The argument useValue
in createContainer
is so flexible
and there are various usages.
This is the most typical usage.
You define a generic reducer and pass reducer
and initialState
as props.
const {
Provider,
useTracked,
// ...
} = createContainer(({ reducer, initialState, init }) => useReducer(reducer, initialState, init));
const reducer = ...;
const App = ({ initialState }) => (
<Provider reducer={reducer} initialState={initialState}>
...
</Provider>
);
For most cases, you would have a static reducer.
In this case, define useValue with the reducer in advance.
The initialState
can be defined in useValue like the following example,
or can be taken from props: ({ initialState }) => useReducer(...)
This is good for TypeScript because the hooks returned by createContainer
is already typed.
const reducer = ...;
const initialState = ...;
const {
Provider,
useTracked,
// ...
} = createContainer(() => useReducer(reducer, initialState));
const App = () => (
<Provider>
...
</Provider>
);
If you don't need reducer, useState would be simpler.
const {
Provider,
useTracked,
// ...
} = createContainer(({ initialState }) => useState(initialState);
const App = ({ initialState }) => (
<Provider initialState={initialState}>
...
</Provider>
);
You could even start with completely an empty object.
This might not be TypeScript friendly. Although, you could do this: useState<State>({})
const {
Provider,
useTracked,
// ...
} = createContainer(() => useState({});
const App = () => (
<Provider>
...
</Provider>
);
Here's how to persist state in localStorage.
const reducer = ...;
const initialState = ...; // used only if localStorage is empty.
const storageKey = 'persistedState';
const init = () => {
let preloadedState;
try {
preloadedState = JSON.parse(window.localStorage.getItem(storageKey));
// validate preloadedState if necessary
} catch (e) {
// ignore
}
return preloadedState || initialState;
};
const useValue = () => {
const [state, dispatch] = useReducer(reducer, null, init);
useEffect(() => {
window.localStorage.setItem(storageKey, JSON.stringify(state));
}, [state]);
return [state, dispatch];
};
const {
Provider,
useTracked,
// ...
} = createContainer(useValue);
const App = () => (
<Provider>
...
</Provider>
);
Using async storage is a bit tricky. See the thread for an example.
If you already have a state and would like to use Provider with it, you can sync a container state with a state from props.
const useValue = ({ propState }) => {
const [state, setState] = useState(propState);
useEffect(() => {
// or useLayoutEffect
setState(propState);
}, [propState]);
return [state, setState];
};
const {
Provider,
useTracked,
// ...
} = createContainer(useValue);
const App = ({ propState }) => <Provider propState={propState}>...</Provider>;
Note that propState
has to be updated immutably.
Here's how to dispatch actions by DOM events.
const reducer = ...;
const initialState = ...;
const useValue = () => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const listener = () => {
dispatch({
type: 'WINDOW_RESIZED',
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', listener);
return () => {
window.removeEventListener('resize', listener);
};
}, []);
return [state, dispatch];
};
const {
Provider,
useTracked,
// ...
} = createContainer(useValue);
const App = () => (
<Provider>
...
</Provider>
);
If you want to have custom update functions,
you can store them in a state object.
Be sure to use useCallback
and useMemo
to make the state object stable.
const useValue = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const state = useMemo(
() => ({
count,
increment,
decrement,
}),
[count, increment, decrement],
);
return [
state,
() => {
throw new Error('use functions in the state');
},
];
};
const { Provider, useTrackedState } = createContainer(useValue);
const App = () => <Provider>...</Provider>;
Note: With custom update functions, you don't get the benefit
even if you enable concurrentMode
in createContainer
.
The useTrackedState
and useTracked
hooks are useful as is,
but new hooks can also be created based on them.
Selector interface is useful to share selection logic. You can create a selector hook with state usage tracking very easily.
const useSelectorWithTracking = (selector) => selector(useTrackedState());
Note: This is different from useSelector
which has no tracking support
and triggers re-render based on the ref equality of selected value.
Sometimes, you might want to select a state by its property name.
Here's a custom hook to return a tuple [value, setValue]
selected by a name.
const useTrackedByName = (name) => {
const [state, setState] = useTracked();
const update = useCallback(
(newVal) => {
setState((oldVal) => ({
...oldVal,
[name]: typeof newVal === 'function' ? newVal(oldVal[name]) : newVal,
}));
},
[setState, name],
);
return [state[name], update];
};
Updating a property deep in a state object is troublesome. Here's a custom hook to use immer for setState.
import produce from 'immer';
const useTrackedWithImmer = () => {
const [state, setState] = useTracked();
const update = useCallback(
(updater) => {
setState((oldVal) => produce(oldVal, updater));
},
[setState],
);
return [state, update];
};
Note: This can also be done at createContainer
.
The useUpdate
simply returns the second item
in a tuple returned by useState
or useReducer
.
It can also be extended as a custom hook.
This is a modified version of useDispatch that calls getUntrackedObject
recursively on an action object before dispatching it.
import { getUntrackedObject } from 'react-tracked';
const untrackDeep = (obj) => {
if (typeof obj !== 'object' || obj === null) return obj;
const untrackedObj = getUntrackedObject(obj);
if (untrackedObj !== null) return untrackedObj;
const newObj = {};
let modified = false;
Object.entries(obj).forEach(([k, v]) => {
newObj[k] = untrackDeep(v);
if (newObj[k] !== null) {
modified = true;
} else {
newObj[k] = v;
}
});
return modified ? newObj : obj;
};
const useSafeDispatch = () => {
const dispatch = useDispatch();
return useCallback(
(action) => {
dispatch(untrackDeep(action));
},
[dispatch],
);
};