React Concepts

children

The children concept in React refers to any child components or elements passed into a parent component. In this example, the TestComponent is rendered by the TestWrapper component, but React does not rerender TestComponent after clicking the button because the props have not changed. React reuses the same component instance as long as its props remain unchanged.

This custom implementation demonstrates how React optimizes rendering by not rerendering TestComponent unless the props passed to it change, despite the state change in the parent component (TestWrapper).

import { ReactElement, useState } from "react";
 
export default function App() {
  return <TestWrapper prop={<TestComponent />} />;
}
 
function TestWrapper({ prop }: { prop: ReactElement }) {
  const [bool, setBool] = useState(false);
  console.log("TestWrapper");
  return (
    <>
      <button onClick={() => setBool(!bool)}>click</button>
      {prop}
    </>
  );
}
 
function TestComponent() {
  console.log("TestComponent");
  return <></>;
}
 
// initial render
// TestWrapper
// TestComponent
 
// after click
// TestWrapper

Normalized State

The concept of normalized state is commonly used in managing complex data structures, such as nested objects or arrays, in a way that makes it easier to update and manage state in applications. This approach involves flattening the structure into a more accessible format, often using an object with unique IDs as keys, and maintaining relationships between the entities via arrays of IDs (as seen in childIds).

In this example, the TravelPlanComponent component simulates a travel planning system where each location (node) has a title and a set of child locations (nested nodes). The normalized state design keeps all the locations in a flat object, and relationships between locations are managed using the childIds array.

The removeNodeAndChildren function recursively removes a node and its children from the state, and handleComplete ensures that a child node is removed from its parent node’s childIds. This architecture makes it easier to update and delete nodes or manage complex state in React applications, especially with deep nested structures.

import { useState } from "react";
 
type TravelNode = {
  id: number;
  title: string;
  childIds: number[];
};
 
type TravelPlan = {
  [key: number]: TravelNode;
};
 
const initialTravelPlan: TravelPlan = {
  0: { id: 0, title: "(Root)", childIds: [1, 42] },
  1: { id: 1, title: "Earth", childIds: [2, 10] },
  2: { id: 2, title: "Africa", childIds: [3, 4, 5] },
  3: { id: 3, title: "Botswana", childIds: [] },
  4: { id: 4, title: "Egypt", childIds: [] },
  5: { id: 5, title: "Kenya", childIds: [] },
  10: { id: 10, title: "Americas", childIds: [11, 12, 13] },
  11: { id: 11, title: "Argentina", childIds: [] },
  12: { id: 12, title: "Brazil", childIds: [] },
  13: { id: 13, title: "Barbados", childIds: [] },
  42: { id: 42, title: "Moon", childIds: [43, 44, 45] },
  43: { id: 43, title: "Rheita", childIds: [] },
  44: { id: 44, title: "Piccolomini", childIds: [] },
  45: { id: 45, title: "Tycho", childIds: [] },
};
 
export default function TravelPlanComponent() {
  const [travelPlan, setTravelPlan] = useState<TravelPlan>(initialTravelPlan);
 
  const removeNodeAndChildren = (id: number, plan: TravelPlan): TravelPlan => {
    const newPlan = { ...plan };
 
    const removeRecursively = (nodeId: number) => {
      const node = newPlan[nodeId];
      if (!node) return;
 
      // Remove all children recursively
      node.childIds.forEach(removeRecursively);
 
      // Delete the current node
      delete newPlan[nodeId];
    };
 
    removeRecursively(id);
    return newPlan;
  };
 
  const handleComplete = (id: number, parentId: number) => {
    setTravelPlan((prevPlan) => {
      const updatedPlan = removeNodeAndChildren(id, prevPlan);
 
      // Remove the child from the parent's childIds array
 
      updatedPlan[parentId] = {
        ...updatedPlan[parentId],
        childIds: updatedPlan[parentId].childIds.filter(
          (childId) => childId !== id,
        ),
      };
 
      return updatedPlan;
    });
  };
 
  return (
    <div>
      {travelPlan[0].childIds.map((childId) => (
        <RenderPlace
          key={childId}
          id={childId}
          parentId={travelPlan[0].id}
          travelPlan={travelPlan}
          handleComplete={handleComplete}
        />
      ))}
    </div>
  );
}
 
const RenderPlace = ({
  id,
  parentId,
  handleComplete,
  travelPlan,
}: {
  id: number;
  parentId: number;
  handleComplete: (id: number, parentId: number) => void;
  travelPlan: TravelPlan;
}) => {
  const node = travelPlan[id];
 
  if (!node) return null;
 
  return (
    <ol style={{ paddingInlineStart: 0 }}>
      <li
        style={{
          display: "flex",
          gap: "1rem",
          alignItems: "center",
        }}
      >
        <h5
          style={{
            marginBlockStart: 5,
            marginBlockEnd: 5,
          }}
        >
          {node.title}
        </h5>
        <button onClick={() => handleComplete(node.id, parentId)}>
          Complete
        </button>
      </li>
      {node.childIds.length > 0 && (
        <ol>
          {node.childIds.map((childId) => (
            <li key={childId}>
              <RenderPlace
                id={childId}
                parentId={node.id}
                travelPlan={travelPlan}
                handleComplete={handleComplete}
              />
            </li>
          ))}
        </ol>
      )}
    </ol>
  );
};

usePrevious

The usePrevious custom hook allows you to track the previous value of a state variable in React. This hook leverages the useRef hook to store the previous value of a given state, and useEffect to update the ref value after the component has rendered. This approach makes it possible to access the value before the current one, which is useful for cases where you need to compare the previous and current values of a variable.

In this example, the usePrevious hook tracks the previous count state value. The value of the ref is updated inside the useEffect, which runs after the component has rendered. The previous value is stored in the ref and is returned by the hook.

import { useState, useEffect, useRef } from "react";
 
function usePrevious(value: string | number) {
  const ref = useRef<string | number>();
 
  useEffect(() => {
    ref.current = value;
    console.log("useEffect", ref.current);
    return () => {
      console.log("useEffect return");
    };
  }, [value]);
 
  console.log({ ref: ref.current });
 
  return ref.current;
}
 
function App() {
  const [count, setCount] = useState(0);
  console.log({ count });
  const previousCount = usePrevious(count);
 
  return (
    <div>
      <p>Count: {count}</p>
      <p>Previous count: {previousCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
 
export default App;
// initial render logs
 
// {count: 0}
// {ref: undefined}
// useEffect 0
// useEffect return
// useEffect 0
 
// consequent renders log
 
// {count: 1}
// {ref: 0}
// useEffect return
// useEffect 1

useReducer

The useReducer hook is a more advanced alternative to useState for managing complex state logic in React. It allows you to handle state transitions using a reducer function, similar to how Redux works. useReducer is especially useful when the state depends on previous state values or involves more complex state updates.

In this example, useReducer is used to manage a collection of messages. The messengerReducer function defines the logic for adding and removing messages. The reducer receives the current state and an action, then returns the updated state based on the action type.

The useReducer hook returns the current state and a dispatch function to trigger state updates. The state is updated when a button is clicked, either to add a new message or remove the last message.

import { useState } from "react";
 
type Messages = Record<number, string>;
 
type Action =
  | { type: "add_message"; message: string }
  | { type: "remove_last_message" };
 
const initialState: Messages = {
  0: "Hello, Taylor",
  1: "Hello, Alice",
  2: "Hello, Bob",
};
 
function useReducer<S, A>(
  reducer: (state: S, action: A) => S,
  initialState: S,
): [S, (action: A) => void] {
  const [state, setState] = useState<S>(initialState);
 
  function dispatch(action: A) {
    const nextState = reducer(state, action);
    setState(nextState);
  }
 
  return [state, dispatch];
}
 
function messengerReducer(state: Messages, action: Action): Messages {
  switch (action.type) {
    case "add_message": {
      const nextId = Math.max(0, ...Object.keys(state).map(Number)) + 1;
      return {
        ...state,
        [nextId]: action.message,
      };
    }
    case "remove_last_message": {
      const ids = Object.keys(state).map(Number);
      if (ids.length === 0) return state;
      const lastId = Math.max(...ids);
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { [lastId]: _, ...rest } = state;
      return rest;
    }
    default:
      throw new Error("Unknown action: " + (action as Action).type);
  }
}
 
export default function App() {
  const [messages, dispatch] = useReducer<Messages, Action>(
    messengerReducer,
    initialState,
  );
 
  return (
    <div>
      <ul>
        {Object.entries(messages).map(([id, message]) => (
          <li key={id}>{message}</li>
        ))}
      </ul>
      <button
        onClick={() =>
          dispatch({ type: "add_message", message: "New Message" })
        }
      >
        Add Message
      </button>
      <button onClick={() => dispatch({ type: "remove_last_message" })}>
        Remove Last Message
      </button>
    </div>
  );
}