Skip to content

(Micro) State

LeoTM edited this page Apr 19, 2023 · 48 revisions

Capture

aka client/UI state

React Concurrent Mode and Suspense (Nov19) Experimental

Architecture

  • Pub-sub
    • store (shared/global)
    • components (immutable React/JSX.element attribs/children)
  • Reactive / unidirectional (top-down, bottom-up) / bidirectional
  • Persistence (offline)
  • Mutability
  • Progressive Hydration (more)
  • Memoising / React DOM re-rendering/updating
    • Comparison/Reconciliation
  • (Skipped) React lifecycle
  • Code Structure
  • Project specs/size

And so it begins...

this.setState (13)

  • Read-Update's
  • classy 🎩 old skool

Redux (15)

  • top-down
  • global store obj
  • predictable (restrictive) state mutations
  • immutable state (emit actions of pure fn reducers)
  • low boilerplate (RTK 18)
  • redux-observable for RxJS

How well does Redux β€œscale” in terms of performance and architecture?​

  • while no single answer, most of the time shouldn't be a concern
  • heavily optimised to cut down unnecessary re-renders
  • not as efficient OOTB compared to other libs
  • for max rendering perf in a React, minimising overall rendering needed
    • store state normalised
    • many individual components should be connected to the store instead of just a few
    • connected list components should pass item IDs to their connected child list items (allowing the list items to look up their own data by ID)
  • memoised selector fn's also an important perf consideration

for scale, we have ~500 action types, ~400 reducer cases, ~150 components, 5 middlewares, ~200 actions, ~2300 tests

Do I have to deep-clone my state in a reducer? Isn't copying my state going to be slow?

How can I reduce the number of store update events?

batch public API is available to help minimize the number of React re-renders when dispatching actions outside of React event handlers. It wraps React's unstable_batchedUpdate() API, allows any React updates in an event loop tick to be batched together into a single render pass. React already uses this internally for its own event handler callbacks. This API is actually part of the renderer packages like ReactDOM and React Native, not the React core itself

Since React-Redux needs to work in both ReactDOM and React Native environments, we've taken care of importing this API from the correct renderer at build time for our own use

import { batch } from 'react-redux'

function myThunk() {
  return (dispatch, getState) => {
    // should only result in one combined re-render, not two
    batch(() => {
      dispatch(increment())
      dispatch(increment())
    })
  }
}

RxJS (15)

Lodash for events

  • reactive
  • easier async/cb-based code
  • observe (data streams), sub (cb), unsub (cb)
  • not a state manager
  • RN sensors example

MobX (16)

React Easy State (17)

https://github.com/RisingStack/react-easy-state

Recompose (18)

Context (18)

  • deep Read-Update's
  • some data changes rarely (e.g. user/auth, theme, locale) needs sharing to many deeply/shallow nested components
  • subscribe to re-render changes (ignoring shouldComponentUpdate)
  • consider 1st {children} and inversion of control (JSX props)
  • all consumers re-render πŸ’₯ (comparing {} ref id w Object.is), split into small contexts of compound components (e.g. List > Item), or React.memo / useMemo / react-tracked / lifting value to parent state

Effector (18)

ez business logic

  • reactive
  • immutable state

Akita (18)

  • reactive
  • OOP
const ContextA = createContext({ something: 'initValue' })

export const Root = () =>
  <ContextA.Provider value={{ something: 'newValue' }}>
    <App />
  </ContextA.Provider>

const DeepChild = () => {
  const { something } = useContext(ContextA) // subscribe to consume (read)
  return (
    <Text>
      { something }
    </Text>
  )
}

useState (19)

  • Read-Update's
  • component UI state, render initial state/props and changes
  • use Server State lib instead for fetching/loading/error state or GraphQL client
const Counter = ({ initialCount }) => {
  const [count, setCount] = useState(initialCount)
  return (
    <>
      Count: {count}
      <Button onPress={() => setCount(initialCount)}>Reset</button>
      <Button onPress={() => setCount(prevCount => prevCount - 1)}>-</button>
      <Button onPress={() => setCount(prevCount => prevCount + 1)}>+</button>
    </>
  )
}

useReducer (19)

  • complex logic, multiple sub-values, nextState depends on prevState
const initialState = { count: 0 }

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 }
    case 'decrement':
      return { count: state.count - 1 }
    default:
      throw new Error()
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      Count: { state.count }
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  )
}

useReducer + Context (19)

  • large component trees
const TodosDispatch = createContext(null)

const TodosApp = () => {
  // `dispatch` unchanged between re-renders
  const [todos, dispatch] = useReducer(todosReducer)
  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  )
}

const DeepChild = (props) => {
  const dispatch = useContext(TodosDispatch)
  return (
    <button onClick={dispatch({ type: 'add', text: 'hello' })}>Add todo</button>
  )
}

Zustand (19)

import React from 'react'
import create from 'zustand'
import CodePreview from './components/CodePreview'
import Backdrop from './components/Backdrop'
import Details from './components/Details'
import code from './resources/code'

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

function Counter() {
  const { count, inc } = useStore()
  return (
    <div className="counter">
      <span>{count}</span>
      <button onClick={inc}>one up</button>
    </div>
  )
}

export default function App() {
  return (
    <>
      <Backdrop />
      <div className="main">
        <div className="code">
          <div className="code-container">
            <CodePreview code={code} />
            <Counter />
          </div>
        </div>
        <Details />
      </div>
    </>
  )
}

Jotai (20)

  • bottom-up (atomic)
  • within React, useState+useContext alternative
  • if like Zustand
  • if wanna try to create new lib
  • code splitting
  • React 18 βœ”οΈ

https://jotai.org/docs/basics/comparison

Recoil (20)

Valtio (21)

useSelectedContext (21)

https://github.com/dai-shi/use-context-selector

Legend-State (22)

  • reactive
  • 3kb, fast, ez
  • global/local (1 store or atoms)
  • persist

XState/fsm (19)

https://github.com/statelyai/xstate/tree/main/packages/xstate-fsm

Enums (15)