Recipes
React Tracked provides a primitive API, and there are various ways to use it for apps.
Recipes for createContainer
The argument useValue
in createContainer
is so flexible
and there are various usages.
useReducer (props)
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>
);
useReducer (embedded)
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>
);
useState (props)
If you don't need reducer, useState would be simpler.
const {
Provider,
useTracked,
// ...
} = createContainer(({ initialState }) => useState(initialState);
const App = ({ initialState }) => (
<Provider initialState={initialState}>
...
</Provider>
);
useState (empty object)
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>
);
useReducer (with persistence)
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.
useState (with propState)
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.
useReducer (with event listener)
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>
);
useState (with update functions)
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
.
Recipes for useTrackedState and useTracked
The useTrackedState
and useTracked
hooks are useful as is,
but new hooks can also be created based on them.
useSelectorWithTracking
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.
useTrackedByName (based on useState)
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];
};
useTrackedWithImmer (based on useState)
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
.
Recipes for useUpdate (useDispatch)
The useUpdate
simply returns the second item
in a tuple returned by useState
or useReducer
.
It can also be extended as a custom hook.
useSafeDispatch
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],
);
};