DEV Community

Cover image for Best Practices in React with Zustand for State Management
Admin | Xyvin
Admin | Xyvin

Posted on

Best Practices in React with Zustand for State Management

React is a powerful library for building user interfaces, and with tools like Zustand, managing state becomes easier, especially for complex applications. Following best practices in organizing and writing React code ensures your application is maintainable, scalable, and optimized for performance. In this guide, we’ll cover essential practices for React projects, including folder structure, state management, form handling, and useful tools.

1. Organize Your Folder Structure

A clean project structure is fundamental to maintainability and scalability. Here’s an example folder layout for a React project using Zustand for state management:


src
├── api          # API calls and configurations
├── assets       # Images, fonts, icons, etc.
├── components   # Reusable components (buttons, inputs, etc.)
├── hooks        # Custom hooks
├── pages        # Page components (Home, Dashboard, etc.)
├── store        # Zustand stores
├── utils        # Utility functions
├── App.js
├── index.js
└── styles       # Global styles and theme
Enter fullscreen mode Exit fullscreen mode

Key Directories Explained:

  • components: Reusable, presentational components that are not tied to specific pages.
  • pages: Layout and routing logic. Components here represent individual screens, like Home, Dashboard, etc.
  • store: Store configurations for Zustand, separating global state logic.
  • utils: Utility functions, constants, and helper functions for common operations (e.g., date formatting, API endpoint configurations).
  • styles: Contains global styles and themes. This organization provides separation of concerns, keeping each part of the code focused on a specific purpose.

2. State Management with Zustand

Zustand is a lightweight state management library that works well with React. It provides a centralized store without the boilerplate of Redux.

Setting Up Zustand Store:

Create your Zustand store in the store folder. Here’s a basic example of a store for user authentication:

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

const useAuthStore = create((set) => ({
  user: null,
  isAuthenticated: false,
  login: (userData) => set({ user: userData, isAuthenticated: true }),
  logout: () => set({ user: null, isAuthenticated: false }),
}));

export default useAuthStore;
Enter fullscreen mode Exit fullscreen mode

Using Zustand in Components:

Access your store data and actions using the useAuthStore hook within your components:


// components/Header.js
import React from 'react';
import useAuthStore from '../store/authStore';

const Header = () => {
  const { user, logout } = useAuthStore();

  return (
    <header>
      {user ? <p>Welcome, {user.name}!</p> : <p>Please log in</p>}
      {user && <button onClick={logout}>Logout</button>}
    </header>
  );
};

export default Header;
Enter fullscreen mode Exit fullscreen mode

Using Zustand keeps your state management simple, avoiding complex setup and reducing code redundancy.

3. Component Design Principles

To keep your code organized, focus on reusable and modular components:

  • Functional Components: Always use functional components for easier readability and lifecycle management with hooks.
  • Container vs. Presentational Components: Separate your components based on their function—container components manage state and logic, while presentational components focus on UI and layout.
  • Use Memoization: Use React.memo and the useMemo hook to prevent unnecessary re-renders, optimizing performance.

import React, { memo } from 'react';

const Button = memo(({ onClick, children }) => {
  return <button onClick={onClick}>{children}</button>;
});
Enter fullscreen mode Exit fullscreen mode

Memoization is especially useful for components with heavy calculations or frequent renders.

4. Form Handling

React Hook Form is a popular library for handling forms. It’s efficient, minimizes re-renders, and integrates well with Zustand for managing form data.

Installing and Using React Hook Form:

npm install react-hook-form
Enter fullscreen mode Exit fullscreen mode

Basic Form Setup:

// components/LoginForm.js
import React from 'react';
import { useForm } from 'react-hook-form';
import useAuthStore from '../store/authStore';

const LoginForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm();
  const login = useAuthStore((state) => state.login);

  const onSubmit = (data) => {
    login(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('username', { required: true })} placeholder="Username" />
      {errors.username && <p>Username is required</p>}

      <input {...register('password', { required: true })} placeholder="Password" />
      {errors.password && <p>Password is required</p>}

      <button type="submit">Login</button>
    </form>
  );
};

export default LoginForm;
Enter fullscreen mode Exit fullscreen mode

React Hook Form also supports validation and integrates smoothly with Zustand, making form handling more efficient.

5. Reusable Custom Hooks

Custom hooks can help isolate and reuse logic, such as fetching data, authentication checks, or managing form state.

Example: Custom Hook for Fetching Data


// hooks/useFetch.js
import { useState, useEffect } from 'react';

const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      }
    };
    fetchData();
  }, [url]);

  return { data, error };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Use the hook in components to simplify API requests:

const { data, error } = useFetch('/api/users');
Enter fullscreen mode Exit fullscreen mode

Custom hooks make it easy to reuse logic across components without redundancy.

6. Styling Techniques

For styling, consider using CSS-in-JS libraries like Styled Components or a CSS framework like Tailwind CSS. Both options work well for scoped and reusable styles.

Using Styled Components:


npm install styled-components
Enter fullscreen mode Exit fullscreen mode

// components/Button.js
import styled from 'styled-components';

const Button = styled.button`
  padding: 10px 20px;
  background-color: #0070f3;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: #005bb5;
  }
`;

export default Button;
Enter fullscreen mode Exit fullscreen mode

Using Styled Components or Tailwind keeps styles scoped to components and avoids global CSS conflicts.

7. Use ESLint and Prettier

Consistency and readability in code are essential for collaboration. ESLint and Prettier help enforce consistent style and catch syntax errors.

Install ESLint and Prettier:


npm install eslint prettier eslint-config-prettier eslint-plugin-prettier --save-dev
Enter fullscreen mode Exit fullscreen mode

Basic Configuration for ESLint and Prettier:

Create .eslintrcand .prettierrc configuration files in your project root with your preferred rules. Many IDEs support automatic formatting on save with these tools.

8. Optimize Performance

React’s reactivity can lead to performance issues if not managed carefully. Here are some tips:

  • Use React.memo: Prevent unnecessary re-renders by wrapping components in React.memo.
  • Lazy Load Components: Load components only when needed using React.lazy() and Suspense.
const LazyComponent = React.lazy(() => import('./components/LazyComponent'));

<Suspense fallback={<div>Loading...</div>}>
  <LazyComponent />
</Suspense>
Enter fullscreen mode Exit fullscreen mode
  • Avoid Inline Functions: Define functions outside the render method to prevent re-creation on each render.

9. Testing

Use a testing library like Jest and React Testing Library to ensure your application is working as expected.

Installing and Writing Tests:

npm install --save-dev jest @testing-library/react
Enter fullscreen mode Exit fullscreen mode

Example Test Case:


// __tests__/Button.test.js
import { render, screen } from '@testing-library/react';
import Button from '../components/Button';

test('renders button with correct text', () => {
  render(<Button>Click Me</Button>);
  const buttonElement = screen.getByText(/Click Me/i);
  expect(buttonElement).toBeInTheDocument();
});
Enter fullscreen mode Exit fullscreen mode

Testing ensures your code is robust and reduces the likelihood of unexpected behavior in production.

Conclusion

Following best practices in React, combined with Zustand for state management, creates a structured, scalable, and maintainable codebase. A clear folder structure, reusable custom hooks, optimized state handling, and consistency in style and testing will enhance your development process and improve application performance.

Top comments (0)