Routing

React Router

A library to handle navigation inside your website

Works very similarly to React Native routing library, React Navigation

Folder Structure

src/
 ├─ pages/
 │   ├─ HTML/
 │   │   ├─ Basics.js
 │   │   ├─ Forms.js
 │   ├─ CSS/
 │   │   ├─ Basics.js
 │   │   ├─ Properties.js
 ├─ App.js
 ├─ index.js

Wrap BrowserRouter around the root component

// JSX
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Set up the Routes and Link elements

  • Routes acts as the main-content container and will swap out components based on URL
  • Link replaces element <a>
// JSX
import { Routes, Route, Link } from 'react-router-dom';

import HTMLBasics from './pages/HTML/Basics';
import Forms from './pages/HTML/Forms';

import CSSBasics from './pages/CSS/Basics';
import CSSProperties from './pages/CSS/Properties';

export default function App() {
  return (
    <div>
      {/* Sidebar Navigation */}
      <nav>
        <h3>HTML</h3>
        <ul>
          <li><Link to='/html/basics'>HTML Basics</Link></li>
          <li><Link to='/html/forms'>Forms</Link></li>
        </ul>

        <h3>CSS</h3>
        <ul>
          <li><Link to='/css/basics'>CSS Basics</Link></li>
          <li><Link to='/css/properties'>CSS Properties</Link></li>
        </ul>
      </nav>

      {/* Page Content */}
      <main>
        <Routes>
          {/* HTML Routes */}
          <Route path='/html/basics' element={<HTMLBasics />} />
          <Route path='/html/forms' element={<Forms />} />

          {/* CSS Routes */}
          <Route path='/css/basics' element={<CSSBasics />} />
          <Route path='/css/properties' element={<CSSProperties />} />
        </Routes>
      </main>
    </div>
  );
}

State Management

Zustand

Quick and lightweight way to manage state to avoid props drilling and allow multiple components to share states

  • Can store ref, values, objects, functions and avoid props drilling
  • Minimal boilerplate code
  • Supports async actions naturally
  • Can set up small, modular stores
  • Works the same way in React Native
  • When updating the store, Zustand does a shallow merge

Code sample to manage a store with 2 states: theme and user

Setting up the store

// JSX
// store.js
import { create } from 'zustand';

export const useStore = create((set) => ({
  theme: 'light',
  user: { name: '', email: '' },

  // Theme actions
  toggleTheme: () =>
    set((currentState) => ({
      theme: currentState.theme === 'light' ? 'dark' : 'light',
    })),

  // User actions
  setUser: (newUser) => set({ user: newUser }),
  clearUser: () => set({ user: { name: '', email: '' } }),
}));

Updating the store and getting value

// JSX
// App.jsx
import React from 'react';
import { useStore } from './store';

export default function App() {
  const { theme, toggleTheme, user, setUser, clearUser } = useStore();

  return (
    <div>
      <h1>Zustand Example</h1>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>

      <h2>User Profile</h2>
      {user.name ? (
        <div>
          <p>Name: {user.name}</p>
          <p>Email: {user.email}</p>
          <button onClick={clearUser}>Clear User</button>
        </div>
      ) : (
        <button onClick={() => setUser({ name: 'Tran', email: 'tran@example.com' })}>
          Set User
        </button>
      )}
    </div>
  );
}

Redux

Centralized state management for your entire app.

  • Uses a single global store
  • Require high boilerplate code
  • Higher learning curve to understand
  • Works the same way in React Native

Code sample to manage a store with 2 states: theme and user

Setting up the store

// JSX
// store.js
import { configureStore, createSlice } from '@reduxjs/toolkit';

// Theme slice
const themeSlice = createSlice({
  name: 'theme',
  initialState: { mode: 'light' },
  reducers: {
    toggleTheme: (state) => {
      state.mode = state.mode === 'light' ? 'dark' : 'light';
    },
  },
});

// User slice
const userSlice = createSlice({
  name: 'user',
  initialState: { name: '', email: '' },
  reducers: {
    setUser: (state, action) => {
      state.name = action.payload.name;
      state.email = action.payload.email;
    },
    clearUser: (state) => {
      state.name = '';
      state.email = '';
    },
  },
});

export const { toggleTheme } = themeSlice.actions;
export const { setUser, clearUser } = userSlice.actions;

export const store = configureStore({
  reducer: {
    theme: themeSlice.reducer,
    user: userSlice.reducer,
  },
});

Using the store and getting value

// JSX
// App.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { toggleTheme, setUser, clearUser } from './store';

function App() {
  const dispatch = useDispatch();
  const theme = useSelector((state) => state.theme.mode);
  const user = useSelector((state) => state.user);

  return (
    <div>
      <h1>Redux Example</h1>
      <p>Current theme: {theme}</p>
      <button onClick={() => dispatch(toggleTheme())}>Toggle Theme</button>

      <h2>User Profile</h2>
      {user.name ? (
        <div>
          <p>Name: {user.name}</p>
          <p>Email: {user.email}</p>
          <button onClick={() => dispatch(clearUser())}>Clear User</button>
        </div>
      ) : (
        <button onClick={() => dispatch(setUser({ name: 'Tran', email: 'tran@example.com' }))}>
          Set User
        </button>
      )}
    </div>
  );
}

Data Fetch & Server-State Management

React Query (TanStack Query)

React Query is a powerful library for data fetching and managing server-side state. It helps you:

  • Fetch, cache, and update server data efficiently
  • Auto-refetch when data changes or the user focuses the window
  • Simplifies managing loading, success, and error states
  • Works seamlessly with REST APIs and GraphQL

Installation

# Install React Query
npm install @tanstack/react-query

Setup the QueryClientProvider

// JSX
// main.jsx or index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
);

Fetching data with useQuery

// JSX
// App.jsx
import React from 'react';
import { useQuery } from '@tanstack/react-query';

async function fetchUsers() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users');
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
}

export default function App() {
  const { data, isLoading, isError, error, refetch } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    staleTime: 5000, // Data stays fresh for 5 seconds
  });

  if (isLoading) return <p>Loading users...</p>;
  if (isError) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h1>React Query Example</h1>
      <button onClick={refetch}>Refetch Users</button>
      <ul>
        {data.map((user) => (
          <li key={user.id}>
            {user.name}{user.email}
          </li>
        ))}
      </ul>
    </div>
  );
}

Posting data with useMutation

// JSX
// CreateUser.jsx
import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';

async function createUser(newUser) {
  const response = await fetch('https://jsonplaceholder.typicode.com/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newUser),
  });
  if (!response.ok) {
    throw new Error('Failed to create user');
  }
  return response.json();
}

export default function CreateUser() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const queryClient = useQueryClient();

  const { mutate, isPending, isError, error } = useMutation({
    mutationFn: createUser,
    onSuccess: (data) => {
      // Invalidate the users cache to refetch updated data
      queryClient.invalidateQueries({ queryKey: ['users'] });
      alert(`User ${data.name} created successfully!`);
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    mutate({ name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Create User</h2>
      <input
        type='text'
        placeholder='Name'
        value={name}
        onChange={(e) => setName(e.target.value)}
        required
      />
      <input
        type='email'
        placeholder='Email'
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <button type='submit' disabled={isPending}>
        {isPending ? 'Creating...' : 'Create User'}
      </button>
      {isError && <p style={{ color: 'red' }}>Error: {error.message}</p>}
    </form>
  );
}

Automatic refetching

  • React Query automatically refetches data when:
    • The window regains focus
    • The network reconnects
    • The query becomes stale

Summary

  • useQuery → Fetch and cache data
  • useMutation → Create, update, or delete data
  • invalidateQueries → Refresh cache manually
  • staleTime and cacheTime → Control caching