Upgrade to Pro — share decks privately, control downloads, hide ads and more …

React State Management

React State Management

Overview of state of state management in 2021

3ca9eefaaf23fe04664846cb0d5c42ba?s=128

Dmitri Pisarev

May 06, 2021
Tweet

Transcript

  1. React Best Practices The great shift you might have missed

    in the recent years Dmitri Pisarev, Neos Conference 2021
  2. - Christian believer ❤ - Into web dev since 2005

    - Volunteer @ SFI.ru - Part-time @ DEED - team member Dmitri Pisarev @dimaip dimaip.github.io
  3. Immutability & type safety • TypeScript + typescript-eslint, as strict

    as possible • GraphQL + codegen • But… Next time!
  4. Yes, State Management

  5. https://github.com/olegrjumin/awesome-react-state-management Mental Health, anyone?

  6. Once upon a time… (state) => view

  7. Define state management? • store an initial value • read

    the current value • update a value
  8. Passing state down via props-drilling PRO: • very explicit CONTRA:

    • cumbersome in a large app const ThemedButton = ({theme, setTheme}) => { const isLight = theme === "light" return ( <button onClick={() => setTheme(isLight ? "dark" : "light")} style={{ background: isLight ? "white" : "black", color: isLight ? "black" : "white" }} > Set {isLight ? "dark" : "light"} theme </button> ); }; const ManyLevelsOfNesting = ({theme, setTheme}) => <div><ThemedButton theme={theme} setTheme={setTheme} /></div> const App = () => { const [theme, setTheme] = React.useState("light"); return ( <ManyLevelsOfNesting theme={theme} setTheme={setTheme} /> ); };
  9. Context API const ThemeContext = React.createContext(undefined); const ThemedButton = ()

    => { const [theme, setTheme] = React.useContext(ThemeContext); const isLight = theme === "light" return ( <button onClick={() => setTheme(isLight ? "dark" : "light")} style={{ background: isLight ? "white" : "black", color: isLight ? "black" : "white" }} > Set {isLight ? "dark" : "light"} theme </button> ); }; const ManyLevelsOfNesting = () => <div><ThemedButton /></div> const App = () => { const themeTuple = React.useState("light"); return ( <ThemeContext.Provider value={themeTuple}> <ManyLevelsOfNesting /> </ThemeContext.Provider> ); }; PRO • easy to make things globally available • easier to prevent re-renders CONTRA • hard to optimise performance, e.g. create multiple contexts dynamically
  10. Dynamic Contexts Problem

  11. Is Redux Dead? import { Provider, useDispatch, useSelector } from

    "react-redux" import { configureStore, createSlice } from "@reduxjs/toolkit" const themeSlice = createSlice({ name: "theme", initialState: { theme: "light", }, reducers: { toggleTheme(state) { state.theme = state.theme === "light" ? "dark" : "light" }, }, }) const store = configureStore({ reducer: themeSlice.reducer, }) const ThemedButton = () => { const theme = useSelector((state) => state.theme) const dispatch = useDispatch() const isLight = theme === "light" return ( <button onClick={() => dispatch(themeSlice.actions.toggleTheme())} style={{ background: isLight ? "white" : "black", color: isLight ? "black" : "white", }} > Set {isLight ? "dark" : "light"} theme </button> ) } const ManyLevelsOfNesting = () => ( <div><ThemedButton /></div> ) const App = () => ( <Provider store={store}> <ManyLevelsOfNesting /> </Provider> ) PRO • Global state •Declarative state mutations •Devtools & ecosystem •Battle-tested CONTRA •Boilerplate
  12. Doesn’t Redux use context? • Only to pass down the

    store • Watch out for zombie children! function useSelector(selector) { const [, forceRender] = useReducer((counter) => counter + 1, 0); const { store } = useContext(ReactReduxContext); const selectedValueRef = useRef(selector(store.getState())); useLayoutEffect(() => { const unsubscribe = store.subscribe(() => { const storeState = store.getState(); const latestSelectedValue = selector(storeState); if (latestSelectedValue !== selectedValueRef.current) { selectedValueRef.current = latestSelectedValue; forceRender(); } }); return unsubscribe; }, [store]); return selectedValueRef.current; } “My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.” — Sebastian Markbage
  13. Recoil • boilerplate-free • flexible shared state • app-wide state

    observation (e.g. analytics) "Well, I know that on one tool we saw a 20x or so speedup compared to using Redux. This is because Redux is O(n) in that it has to ask each connected component whether it needs to re-render, whereas we can be O(1)."
  14. const themeAtom = atom({ key: 'themeState', default: 'light', }); const

    ThemedButton = () => { const [theme, setTheme] = useRecoilState(themeAtom); const isLight = theme === "light" return ( <button onClick={() => setTheme(isLight ? "dark" : "light")} style={{ background: isLight ? "white" : "black", color: isLight ? "black" : "white" }} > Set {isLight ? "dark" : "light"} theme </button> ); }; const ManyLevelsOfNesting = () => <div><ThemedButton /></div> const App = () => { return ( <RecoilRoot> <ManyLevelsOfNesting /> </RecoilRoot> ); }; const themeAtom = atom('light'); const ThemedButton = () => { const [theme, setTheme] = useAtom(themeAtom); const isLight = theme === "light" return ( <button onClick={() => setTheme(isLight ? "dark" : "light")} style={{ background: isLight ? "white" : "black", color: isLight ? "black" : "white" }} > Set {isLight ? "dark" : "light"} theme </button> ); }; const ManyLevelsOfNesting = () => <div><ThemedButton /></div> const App = () => { const themeTuple = React.useState("light"); return ( <JotaiProvider> <ManyLevelsOfNesting /> </JotaiProvider> ); }; Recoil Jotai
  15. zustand • single store approach • state lives outside of

    React, great to serve as bridge with non-React world const [useStore] = create((set) => ({ theme: 'light', toggleTheme: () => set((state) => ( { theme: state.theme === 'light' ? 'dark' : 'light' } )) })) const ThemedButton = () => { const { theme, toggleTheme } = useStore() const isLight = theme === 'light' return ( <button onClick={toggleTheme} style={{ background: isLight ? 'white' : 'black', color: isLight ? 'black' : 'white' }}> Set {isLight ? 'dark' : 'light'} theme </button> ) } const ManyLevelsOfNesting = () => ( <div> <ThemedButton /> </div> ) const App = () => { return <ManyLevelsOfNesting /> }
  16. Effect Management

  17. useEffect const PostsList = () => { const [loading, setLoading]

    = useState(false) const [error, setError] = useState(false) const [posts, setPosts] = useState([]); useEffect(() => { setLoading(true) fetch("https://jsonplaceholder.typicode.com/posts") .then((r) => r.json()) .then((r) => { setPosts(r) setLoading(false) setError(false) }).catch(e => { setLoading(false) setError(true) }) }, []); if (postsError) { return <div>Error</div>; } if (loading) { return <div>Loading...</div>; } return ( <ul> {posts.map((post) => ( <li id={post.id}>{post.title}</li> ))} </ul> ); };
  18. Effects with Redux import { combineReducers } from "redux"; import

    { Provider, useDispatch, useSelector } from "react-redux"; import { configureStore, createSlice } from "@reduxjs/toolkit"; const postsSlice = createSlice({ name: "posts", initialState: { isFetching: false, posts: [], error: false }, reducers: { fetchStarted(state) { state.isFetching = true; }, fetchComplete(state, action) { state.posts = action.payload; state.isFetching = false; state.error = null; }, fetchFailed(state, action) { state.error = action.payload; state.isFetching = false; state.posts = []; } } }); const rootReducer = combineReducers({ posts: postsSlice.reducer }); const store = configureStore({ reducer: rootReducer }); const fetchPosts = () => async (dispatch) => { const { fetchStarted, setPosts, setError } = postsSlice.actions; dispatch(fetchStarted()); fetch("https://jsonplaceholder.typicode.com/posts") .then((r) => r.json()) .then((posts) => dispatch(fetchComplete(posts))) .catch((e) => dispatch(fetchFailed(e.toString()))); }; const PostList = () => { const posts = useSelector((state) => state.posts); const dispatch = useDispatch(); useEffect(() => { dispatch(fetchPosts()); }, []); return ( <ul> {posts.posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }; export default function App() { return ( <Provider store={store}> <PostList /> </Provider> ); } • redux-saga • redux-observable • redux-loop • etc
  19. Redux ecosystem • redux-saga • redux-observable • redux-loop • etc

  20. Recoil + Suspense • Selector can return a Promise •

    Use Suspend to handle loading state CONTRA • We loose event semantics import { RecoilRoot, selector, useRecoilValue } from "recoil"; const postsQuery = selector({ key: "posts", get: () => fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()) }); function PostsList() { const posts = useRecoilValue(postsQuery); return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } const App = () => ( <RecoilRoot> <ErrorBoundary> <React.Suspense fallback={<div>Loading...</div>}> <PostsList /> </React.Suspense> </ErrorBoundary> </RecoilRoot> );
  21. RxJS import { of, timer, zip } from "rxjs"; import

    { map, switchMap, catchError, startWith } from "rxjs/operators"; import { fromFetch } from "rxjs/fetch"; import { useObservableState } from "observable-hooks"; const delayedFetch = (url) => { return zip(fromFetch(url).pipe(switchMap((r) => r.json())), timer(300)).pipe( map(([data]) => data) ); }; const PostsList = () => { const [ui] = useObservableState(() => delayedFetch("https://jsonplaceholder.typicode.com/posts").pipe( map((posts) => ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> )), catchError(() => of(<div>ERROR</div>)), startWith(<div>loading...</div>) ) ); return <div>{ui}</div>; } • learn once use anywhere • but steep learning curve • super powerful abstraction
  22. xState • very strong, math- backed abstraction import { useMachine

    } from '@xstate/react' import { Machine, assign } from 'xstate' const postsMachine = Machine({ id: 'posts', initial: 'idle', context: { posts: [], error: null, }, states: { idle: { on: { FETCH: 'loading', }, }, loading: { invoke: { id: 'getPosts', src: (context, event) => fetch('https://jsonplaceholder.typicode.com/posts').then((r) => r.json(), ), onDone: { target: 'success', actions: assign({ posts: (context, event) => event.data }), }, onError: { target: 'failure', actions: assign({ error: (context, event) => event.data }), }, }, }, success: {}, failure: { on: { RETRY: 'loading', }, }, }, }) export default function App() { const [current, send] = useMachine(postsMachine) useEffect(() => { send('FETCH') }, []) if (current.matches('loading') || current.matches('idle')) { return <div>Loading...</div> } return ( <ul> {current.context.posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) }
  23. EffectorJS • state & effect management in one box •

    somewhat simpler than RxJS to learn • state + event + effect manager import { createEffect, createStore } from "effector"; import { useStore } from "effector-react"; const fetchPostsFx = createEffect(async () => fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()) ); fetchPostsFx(); const $posts = createStore([]).on( fetchPostsFx.doneData, (state, posts) => posts ); const $postsError = createStore(null).on( fetchPostsFx.failData, (state, error) => error ); const PostsList = () => { const posts = useStore($posts); const postsError = useStore($postsError); const loading = useStore(fetchPostsFx.pending); if (postsError) { return <div>Error</div>; } if (loading) { return <div>Loading...</div>; } return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }
  24. Types of state • Local component state • Server state

    (cache) • Global app state • Location state • Form state
  25. URL state • A lot of UI state can be

    based on a URL • Keep state on reload
  26. URL + xState const useOnboardingTransition = (): ((action) => void)

    => { const router = useRouter() return (action) => { const currentStep = (router.route.startsWith('/onboarding/') && router.query?.id?.[0]) || 'unknown' const nextStep = machine.transition(currentStep, action).value if (nextStep !== currentStep) { void router.push(`/onboarding/${nextStep}`) } } } const machine = Machine( { key: 'onboarding', initial: 'start', states: { start: { on: { EMAIL: 'create', SSO: 'createSSO', }, }, create: { on: { NEXT: 'details', SSO: 'createSSO', }, }, createSSO: { on: { NEXT: 'details', }, }, waitlist: { type: 'final', }, details: { on: { NEXT: 'assets', }, }, assets: { on: { PREV: 'details', NEXT: 'finished', }, }, finished: { type: 'final', }, unknown: { on: { RESUME: [ { target: 'details', cond: 'onboardingDetailsIncomplete' }, { target: 'assets', cond: 'onboardingAssetsIncomplete' }, { target: 'finished' }, ], }, }, }, }, { guards: { onboardingDetailsIncomplete: (_, event) => 'onboardingDetailsComplete' in event ? !event.onboardingDetailsComplete : false, onboardingAssetsIncomplete: (_, event) => 'onboardingAssetsComplete' in event ? !event.onboardingAssetsComplete : false, }, } ) const { t } = useTranslation('offererOnboardingAssets') const transition = useOffererOnboardingTransition() return <button onClick={() => transition('NEXT')}>Next</button>
  27. Server state vs. client state • Server state is just

    a cache • Caching is hard! • Once server state is gone client state often becomes much more lean
  28. Apollo Client / react-query import { useQuery, QueryCache, ReactQueryCacheProvider }

    from "react-query" const PostList = () => { const { status, data } = useQuery("posts", () => fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()) ) if (status === "loading") { return <div>Loading...</div> } if (status === "error") { return <div>Error!</div> } return ( <ul> {data.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) } const queryCache = new QueryCache() const App = () => ( <ReactQueryCacheProvider queryCache={queryCache}> <PostList /> </ReactQueryCacheProvider> )
  29. Reducer Saga Actions

  30. Query Mutation

  31. TL;DR • Not all state is created equal • Use

    specialised solutions for different state kinds • Every abstraction has a cost to pay • Simple abstractions for simple problems, 
 advanced abstractions for advanced ones • don’t be dogmatic and experiment!
  32. @dimaip dimaip.github.io 
 dev.to/dimaip Say Hi!