How to handle application wide state
Prevent Prop Drilling
const [username] = useState("Joe"); <PageComponent username={username} /> <SectionComponent username={username} /> <SomeOtherComponent username={username} />
const [username] = useState("Joe"); <PageComponent username={username} /> <SectionComponent username={username} /> <SomeOtherComponent username={username} />
const [username] = useState("Joe"); <PageComponent username={username} /> <SectionComponent username={username} /> <SomeOtherComponent username={username} />
const [username] = useState("Joe"); <PageComponent username={username} /> <SectionComponent username={username} /> <SomeOtherComponent username={username} />
Context must be created using
const YourContext =
createContext<ValueType>(…)
The parameter is the default value of the context
Provide context in an ancestor component using
<YourContext.Provider value={…}>
… <!-- child components -->
<YourContext.Provider>
Consume within any descendant component using const value = useContext(YourContext)
import { createContext, useContext } from "react";
type ContextType = {
name: string;
setName: (name: string) => void;
};
export const UserContext = createContext<ContextType | undefined>(undefined);
function Parent() {
const [name, setName] = useState("Joe");
return (
<UserContext.Provider value={{ name, setName }}>
<Child />
</UserContext.Provider>)
}
function Child() {
const context = useContext(UserContext);
return (<>
<p>Name: {context?.name}</p> {/* name == "Joe" */}
<button onClick={() => context?.setName("Jane")}>Change Name</button>
</>)
}
Any JavaScript object can be the value of a context.
Default values can be provided to the form like this:
const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({ defaultValues: { username: 'joe' } });
UserContext.ts
type UserContextValue = {
username: string;
setUsername: (username: string) => void;
};
export const UserContext = createContext<UserContextValue | undefined>(undefined);
export function useUserContext(): UserContextValue {
const context = useContext(UserContext);
if (!context) throw new Error("…");
return context;
}
Profile.tsx
function Profile() { const { setUsername } = useUserContext(); const onSubmit: SubmitHandler<FormValues> = (data) => setUsername(data.name); return ( <form onSubmit={handleSubmit(onSubmit)}> … </form> ); }
function Profile() { const { setUsername } = useUserContext(); const onSubmit: SubmitHandler<FormValues> = (data) => setUsername(data.name); return ( <form onSubmit={handleSubmit(onSubmit)}> … </form> ); }
App.tsx
export function App() { const [username, setUsername] = useState(""); return ( <UserContext.Provider value={{ username, setUsername }} > <Router> … </Router> </UserContext.Provider> ); }
export function App() { const [username, setUsername] = useState(""); return ( <UserContext.Provider value={{ username, setUsername }} > <Router> … </Router> </UserContext.Provider> ); }
Layout.tsx
export function Layout() { const { username } = useUserContext(); return ( <div className={styles.root}> <header> <nav> <Link to="/pokemon">Home</Link> | <Link to="/profile">Profile</Link> </nav> {username && <span>{`Hello, ${username}`}</span>} </header> <main> <Outlet /> </main> </div> ); }
export function Layout() { const { username } = useUserContext(); return ( <div className={styles.root}> <header> <nav> <Link to="/pokemon">Home</Link> | <Link to="/profile">Profile</Link> </nav> {username && <span>{`Hello, ${username}`}</span>} </header> <main> <Outlet /> </main> </div> ); }
It depends.
React provides built-in hooks for flux-based state management without having to use Redux .
Takes a reducer function and an initialState
Returns an array with two values, the current state and a dispatch function
const [state, dispatch] = useReducer(reducer, initialState);
Is called with two parameters
function reducer(state: State, action: Action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error("unknown action type");
}
}
type State = {
count: number;
}
type Action = { type: 'increment' | 'decrement' }
Don't count multiple visits to the same pokemon.
usePokeVisit.ts types
export type AddPokeVisitAction = {
type: "add";
value: string;
};
export type ResetPokeVisitAction = {
type: "reset";
};
type PokeVisitAction = AddPokeVisitAction | ResetPokeVisitAction;
export type PokeVisitState = string[];
export const initialState: PokeVisitState = [];
export function pokeVisitReducer(
state: PokeVisitState,
action: PokeVisitAction
): PokeVisitState {
switch (action.type) {
case "add":
return [...state, action.value];
case "reset":
return initialState;
default:
throw new Error("unknown action type");
}
}
import { initialState, pokeVisitReducer } from "./pokeVisitReducer";
const usePokeVisit = () => {
const [state, dispatch] = useReducer(pokeVisitReducer, initialState);
return { state, dispatch };
};
const { state, dispatch } = usePokeVisit();
// state contains all visited pokemon
// dispatch is a function to dispatch actions to the reducer
// (e.g., add a new pokemon, reset the count)
useEffect(() => {
pokemonName && dispatch({
type: "add",
value: pokemonName
});
}, [dispatch, pokemonName]);
const handleClick = () => dispatch({ type: "reset" });
export type PokeVisitState = Set<string>; export const initialState: PokeVisitState = new Set(); export function pokeVisitReducer( state: PokeVisitState, action: PokeVisitAction ): PokeVisitState { switch (action.type) { case "add": return new Set([...Array.from(state), action.value]);; case "reset": return initialState; default: throw new Error("unknown action type"); } }
export type PokeVisitState = Set<string>; export const initialState: PokeVisitState = new Set(); export function pokeVisitReducer( state: PokeVisitState, action: PokeVisitAction ): PokeVisitState { switch (action.type) { case "add": return new Set([...Array.from(state), action.value]);; case "reset": return initialState; default: throw new Error("unknown action type"); } }
A rough overview of the different solutions available
Provide additional features that useReducer does not:
Permit direct state mutation
const state = proxy({ count: 0, text: 'hello' })
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
const observer = { set: function(obj, prop, value) { console.log(obj, prop, value) obj[prop] = value; return true; } }; const pokemon = new Proxy({}, observer); pokemon.name = 'bulbasaur'; // will call the setter
const observer = { set: function(obj, prop, value) { console.log(obj, prop, value) obj[prop] = value; return true; } }; const pokemon = new Proxy({}, observer); pokemon.name = 'bulbasaur'; // will call the setter
const observer = { set: function(obj, prop, value) { console.log(obj, prop, value) obj[prop] = value; return true; } }; const pokemon = new Proxy({}, observer); pokemon.name = 'bulbasaur'; // will call the setter
const observer = { set: function(obj, prop, value) { console.log(obj, prop, value) obj[prop] = value; return true; } }; const pokemon = new Proxy({}, observer); pokemon.name = 'bulbasaur'; // will call the setter
const observer = { set: function(obj, prop, value) { console.log(obj, prop, value) obj[prop] = value; return true; } }; const pokemon = new Proxy({}, observer); pokemon.name = 'bulbasaur'; // will call the setter
const observer = { set: function(obj, prop, value) { console.log(obj, prop, value) obj[prop] = value; return true; } }; const pokemon = new Proxy({}, observer); pokemon.name = 'bulbasaur'; // will call the setter
const observer = { set: function(obj, prop, value) { console.log(obj, prop, value) obj[prop] = value; return true; } }; const pokemon = new Proxy({}, observer); pokemon.name = 'bulbasaur'; // will call the setter
Primary focus on state and selectors
const textState = atom({
key: 'textState',
default: '',
});
const [text, setText] = useRecoilState(textState);
Finite state machines and state charts
Prefer simple solutions for state management
We learned…