State management

State management

Reacting to input with state

  • With React, you won’t modify the UI from code directly.
  • Describe different state the component can be in and do the logic based on that

imperative vs declerative”

imperative : we have to write the exact instructions to manipulate the UI depending on what just happened (eg: explaining turn by turn direction to a driver)”

declerative : declare what you want to show, and the system figures out how to update the UI (eg: telling driver where to go).

Thinking in react

  1. Identify your component’s different visual states
    • prepare a [story book]()
  2. Determine what triggers those state changes
    • Triggers might be system or human generated.
  3. Represent the state in memory using useState
    • use minimum states needed.
  4. Remove any non-essential state variables
    • same as above, reduce relationship between states so that we can use less state varible and use existing one (eg: isSubmitted and !isSubmitted).
    • Reducers let you unify multiple state variables into a single object and consolidate all the related logic!
  5. Connect the event handlers to set the state.

Principles for structuring state

  • Group related state.
    • Combine multiple state to one if needed. const [position, setPosition] = useState({ x: 0, y: 0 });
  • Avoid contradictions in state.
    • if there is a possibility of conflicted (impossible or confusing) states, refactor. insted of const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); use const [status, setStatus] = useState('typing');
  • Avoid redundant state.
    • If one variable can be infered from existing state, dont create a new state. const [firstName, setFirstName] = useState('');const [lastName, setLastName] = useState('');const [fullName, setFullName] = useState(''); fullname is reduntant state hence avoid that.
  • Avoid duplication in state.
    • When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.
  • Avoid deeply nested state.
    • Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.
    • example : use childIds: [2, 10, 19, 26, 34] insted of adding each object in here.
    • Dont just do soft remove . prefer hard remove.

Don’t mirror props in state like function Message({ messageColor }) { const [color, setColor] = useState(messageColor); . if messageColor is changed by parent , child component (Message in here) wont be aware of that . we can consider using messageColor as an initial value. we can directly use messageColor in our child insted on mirroring.

Sharing state between components

  • Moving the state we need manage for childern to the immediate parent is a way to achive this aka lifting state up.
  • controlled and uncontrolled component: a component which can be managed by parent is called controlled.
  • For each state we have to identify its owner, it might change as the app evolves, or in another term we have to make it a single source of truth.

preserving and resetting state

  • In React, each component (even if we have same component twice in a render tree) on the screen has fully isolated state.
  • If one component is removed from the tree, its and its childern’s states are also destroyed.
  • If we replace one component with same componet in the react UI tree, its state is preserved.
  • Don’t nest component function definitions , when a component is rendereed its nested component (say A) definition is rerun creating it new ‘instance’, which will destroy the state.
export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() { <-- nested component fucntion
    const [text, setText] = useState('');
    
    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
}
return (
    <>
      <MyTextField /> <-- recreated in every rerender
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}


  • If we want to change state even if we want to keep the component in position
    • render the component in another place
    • give each component different key
      • Specifying a key tells React to use the key itself as part of the position, instead of their order within the parent.

Preserving state for removed components

  • Dont remove component.(use css to hide).
  • save message in parent (lift the state up).
  • use local storage.

State Reducers

  • move that state logic into a single function outside your component, called a “reducer”.
  1. specify “what the user just did” by dispatching “actions” from your event handlers.
function handleAddTask(text) {
  dispatch(
    // action object
    {
    type: 'added',
    id: nextId++,
    text: text,
  }
  );
}
  1. write a reducer(?) function (we can use if/else or switch(preferrd as a convention))

Reducer take the state so far and the action, and return the next state.

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
  }
}

  1. use Reducer
import { useReducer } from 'react';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer vs useState

CriteriauseStateuseReducer
Code SizeLess code upfront.Requires writing a reducer function and dispatch actions. Can reduce code if many event handlers modify state similarly.
ReadabilityEasy to read for simple state updates. Can bloat component code for complex updates.Separates update logic from event handlers, improving readability for complex updates.
DebuggingDifficult to trace where state was set incorrectly.Can log every state update and action in the reducer, making it easier to debug. Requires stepping through more code.
TestingState updates are tied to the component.Reducer is a pure function, allowing for isolated testing. Useful for complex state update logic.
PreferenceSome prefer useState for its simplicity.Others prefer useReducer for its structure. Both are interchangeable based on preference.

Note

  • Reducers must be pure (similar to state updater fucntion)
    • They should not send requests, schedule timeouts, or perform any side effects.
    • They should update objects and arrays without mutations.
  • Each action describes a single user interaction

Reducers using Immer

  • Immer provides you with a special draft object which is safe to mutate.

React “Context”

  • Context lets the parent component make some information available to any component in the tree below it.

  • We can avoid props drilling (passing props to nodes by skipping some parent nodes).

    1. Create Context
    import { createContext } from 'react';
    export const LevelContext = createContext(1);
    
    1. Use Context
      const level = useContext(LevelContext);
    
    1. Provide Context (“if any component inside this <Section> asks for LevelContext, give them this level). it will use nearest <LevelContext.Provider> in the UI tree above it
     export default function Section({ level, children }) {
     return (
       <section className="section">
         <LevelContext.Provider value={level}>
           {children}
         </LevelContext.Provider>
       </section>
     );
    
  • We can use and provide a context in the same component.

  • Context will passes through intermediate components.

  • Overiding one context provider with another provider for a nodes parent is possible. Alternate approach for context

  • Prop drilling

  • Extract components and pass JSX as children to them.

Use cases for context

  • Theming (light and dark mode etc.)
  • Current account (auth token / user profile etc)
  • Routing
  • Managing state

Reducer and Context

  • We can use reducer in a context to manage complex screens.
  • Put all the state and dispatch function to a context and use it everywhere down the tree.
  • Steps to implement are exactly same as above. see example
== TaskContext.js ==
import { createContext } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  ); 

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}
// task reducer implementation