All posts

React Components, Hooks, Testing, and State

7 Sep 20262 min readStudy Notes

React Components, Hooks, Testing, and State

Why React

React is an open-source JavaScript library — not a full framework — for building interactive UIs out of small, reusable pieces called components.

  • Component-based — the UI is a tree of independent, composable pieces
  • Declarative — you describe what the UI should look like for a given state, and React figures out how to update the DOM
  • JSX — an HTML-like syntax extension that compiles down to plain JavaScript function calls
  • One-way data flow — data moves down from parent to child through props, which keeps state predictable
  • Hooks — let function components manage state and side effects without ever writing a class

Components, props, and state

function Greeting({ name }) {              // props are read-only inputs
  const [count, setCount] = useState(0);   // state can change over time

  return (
    <div>
      <p>Hello, {name}!</p>
      <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
    </div>
  );
}
  • Props are passed down from a parent and are read-only — a child can never modify its own props.
  • State belongs to a component and changing it (via its setter) triggers a re-render.
  • Local state lives in one component; shared state is lifted up to a common parent (or a store) so multiple components can coordinate.

Hooks and the component lifecycle

Function components don't have lifecycle methods, but hooks cover the same needs:

useEffect(() => {
  const controller = new AbortController();
  fetch('/api/data', { signal: controller.signal })
    .then(res => res.json())
    .then(setData);

  return () => controller.abort(); // cleanup on unmount
}, []); // empty array -> run once, on mount
  • Mounting — the component initializes, sets up state, and runs any one-time side effects
  • Updating — a state or prop change re-runs the function body and re-evaluates the JSX
  • Unmounting — cleanup functions returned from useEffect run here (cancel subscriptions, timers, requests)

useState manages local state, useEffect handles side effects, and useReducer steps in when state transitions get complex enough that a single setState call isn't enough.

Project structure and styling

Modern React projects are commonly scaffolded with Vite, which is faster than the older Create React App because it serves source files directly over native ES modules instead of bundling everything up front:

npm create vite@latest my-app -- --template react
my-app/
├─ public/         # static assets
├─ src/
│  ├─ main.jsx     # entry point
│  └─ App.jsx      # root component
├─ package.json
└─ vite.config.js

Styling options range from simple to structured:

  • Inline styles — a JS object passed to style={{ ... }}
  • CSS Modules — Component.module.css, scoped automatically to that component
  • Styled-components / CSS-in-JS — styles defined alongside the component, with full access to props

Testing and app-wide state

Modern React tests focus on user-visible behavior, not implementation details — because concurrent rendering means updates aren't always synchronous:

test('increments the counter on click', async () => {
  render(<Counter />);
  await userEvent.click(screen.getByRole('button'));
  expect(await screen.findByText('Clicked 1 times')).toBeInTheDocument();
});
  • Prefer findBy* queries and waitFor over asserting on the DOM immediately after an interaction.
  • Simulate real interactions (clicks, typing, network responses) rather than calling internal methods directly.

For state shared across many components, Redux Toolkit cuts the boilerplate of classic Redux:

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: { increment: (state) => { state.value += 1 } },
});

const store = configureStore({ reducer: counterSlice.reducer });

Tip: createSlice() lets you "mutate" state directly inside a reducer — Redux Toolkit uses Immer under the hood to turn that into a safe, immutable update.

More posts