w3bdesign / dfweb-v4

🏡 DFWeb personal portfolio version 4 with Next.js (App router), Sanity.io, React Hook Form, Framer Motion, Storybook, Tailwind CSS, Cypress, Playwright and more. 99% test coverage.
https://www.dfweb.no
MIT License
0 stars 0 forks source link

Refactor? #254

Closed w3bdesign closed 2 weeks ago

w3bdesign commented 1 month ago

React Component Best Practices

  1. Using React Fragments

When returning multiple elements from a component, use React Fragments instead of unnecessary divs:

return (
  <>
    <button>Button 1</button>
    <button>Button 2</button>
  </>
);

Benefits:

  1. Avoiding Layout Styles in Reusable Components

Don't add layout styles (margin, flexbox, grid) to reusable components like buttons or headings.

Instead, use one of these approaches: a. Add a wrapper div with layout styles:

<div className="mb-28">
  <H1>Events in Austin</H1>
</div>

b. Use a className prop for flexibility:

function H1({ children, className }) {
  return (
    <h1 className={cn("text-4xl font-bold", className)}>
      {children}
    </h1>
  );
}

// Usage
<H1 className="mb-28">Events in Austin</H1>
  1. Using TypeScript

Implement TypeScript to catch errors early and improve code quality:

type ButtonProps = {
  buttonType: 'primary' | 'secondary';
  // Other props...
};

function Button({ buttonType, ...props }: ButtonProps) {
  // Component logic
}

Benefits:

By following these best practices, you can create more maintainable, flexible, and robust React components.

Optimizing React Component Structure and State Management

Preventing Prop Drilling

Using the Children Pattern

Instead of passing props through multiple layers of components, we can use the children pattern:

<Sidebar>
  <AddTodoForm setTodos={setTodos} />
</Sidebar>

This approach:

Considerations

Proper State Updates

Using the Updater Function

When updating state based on its previous value, use the updater function:

setTodos(prevTodos => [
  ...prevTodos,
  { id: prevTodos.length + 1, content, isCompleted: false }
]);

Benefits:

Handling Multiple Actions on State Change

Instead of passing the setter function directly, create a custom function to handle multiple actions:

const handleAddTodo = (content) => {
  setTodos(prevTodos => [
    ...prevTodos,
    { id: prevTodos.length + 1, content, isCompleted: false }
  ]);

  if (todos.length === 10) {
    setModalOpen(true);
    setModalMessage("You've added 10 todos. Please upgrade to Pro to add more!");
  }
};

This approach:

By implementing these practices, you can create more maintainable and efficient React components while avoiding common pitfalls in state management and prop passing.

React Best Practices

1. Avoid Hardcoded Values

The Problem with Magic Numbers and Strings

When building React applications, it's crucial to avoid hardcoding values directly into components. This practice, often referred to as using "magic numbers" or "magic strings," can lead to maintenance issues and make your code less scalable.

Example: To-Do List Application

Consider a to-do list application where we want to limit free users to adding only three items. Initially, we might implement this directly in the component:

const AddTodoForm = () => {
  // ... other code

  const handleSubmit = (e) => {
    e.preventDefault();
    if (todos.length >= 3 && !isAuthenticated) {
      alert("You need to sign in to add more than 3 to-dos");
      return;
    }
    // ... add todo logic
  };

  // ... rest of the component
};

The Better Approach: Extracting Constants

Instead of hardcoding values, it's better to define constants:

  1. Initially, at the top of the file:
const MAXIMUM_FREE_TODOS = 3;

const AddTodoForm = () => {
  // ... component logic using MAXIMUM_FREE_TODOS
};
  1. Even better, in a dedicated constants file:

Create a file, e.g., src/library/constants.js:

export const MAXIMUM_FREE_TODOS = 3;

Then import and use in your component:

import { MAXIMUM_FREE_TODOS } from '../library/constants';

const AddTodoForm = () => {
  // ... use MAXIMUM_FREE_TODOS in your logic
};

Benefits of Using Constants

  1. Easier maintenance: Change values in one place.
  2. Improved readability: Descriptive constant names explain the purpose.
  3. Consistency: Ensures the same value is used throughout the application.
  4. Scalability: Facilitates managing multiple constants as the app grows.

By following this practice, you separate configuration from logic, making your React components more maintainable and your codebase more robust.

Improving React Data Fetching and Component Structure

Data Fetching with React Query

When fetching data in React, using useEffect for every request can lead to inefficient network calls. React Query offers a more optimized solution:

  1. Replace useEffect with React Query's useQuery hook.
  2. Implement caching to avoid unnecessary network requests.
  3. Benefit from automatic refetching and other built-in features.

Example:

const { data, isLoading } = useQuery(['jobItem', activeId], fetchJobItem);

Best Practices for Data Fetching

  1. Use third-party libraries like React Query or SWR for efficient data management.
  2. Consider Next.js for built-in data fetching and caching solutions.
  3. Avoid messy, large components with complex data fetching logic.

Improving Component Structure

Breaking Down Large Components

  1. Create smaller, focused components even if they're not reusable.
  2. Use meaningful component names to describe their purpose.

Example:

// Before
{todos.length === 0 ? (
  <p>No todos yet. Add a new one!</p>
) : null}

// After
{!todos.length && <StartScreen />}

Extracting Repeated Markup

  1. Create separate components for repeated markup patterns.
  2. Pass necessary data as props to these components.

Example:

// Before
{todos.map((todo) => (
  <li key={todo.id}>
    {/* Complex markup */}
  </li>
))}

// After
{todos.map((todo) => (
  <TodoItem key={todo.id} todo={todo} />
))}

Typing in TypeScript

Properly type your components and data structures:

type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

const [todos, setTodos] = useState<Todo[]>([]);

By following these practices, you can create more maintainable, efficient, and readable React applications.

Best Practices for React Development

Using Constants and Variables

Benefits of Using Constants

Example: Sensitive Words Check

const sensitiveWords = ['password', 'credit card', 'social security'];

const checkForSensitiveWords = (content) => {
  return sensitiveWords.some(word => content.includes(word));
};

State Initialization

Using Constants for Initial State

const initialTodos = [
  { id: 1, text: 'Learn React', completed: false },
  { id: 2, text: 'Build a project', completed: false }
];

const [todos, setTodos] = useState(initialTodos);

Benefits of Defining Constants Outside Components

Folder Structure Best Practices

Key Principles

Recommended Structure

Notes on Folder Structure

Component Creation Best Practices

When to Create Components

Tips for Component Creation

By following these best practices, you can create more organized, maintainable, and efficient React applications. Remember that these guidelines are flexible and should be adapted to your specific project needs.

Here's an improved and structured version of the transcription:

Best Practices for State Management in React

Using Update Functions with useState

When updating state that depends on the previous state, it's best to use the update function approach:

setCount(prevCount => prevCount + 1)

This practice is recommended because:

  1. It ensures the state update works as expected.
  2. It handles multiple setter calls correctly.
  3. It works well with React's state batching.

Managing Complex State

Boolean States vs. Union Types

Instead of using multiple boolean states (e.g., isLoading, isError), consider using a single state with a union type:

type Status = 'idle' | 'loading' | 'error' | 'success';
const [status, setStatus] = useState<Status>('idle');

Benefits:

Managing Selected Items in Lists

When dealing with lists where items can be selected:

  1. Don't store the entire selected object as state
  2. Instead, store the ID of the selected item
const [selectedTodoId, setSelectedTodoId] = useState(null);

Benefits:

const selectedTodo = todos.find(todo => todo.id === selectedTodoId);

State vs. URL Parameters

For certain types of data, especially those that should be shareable or bookmarkable, consider using URL parameters instead of state:

Example: Color selection on an e-commerce site

Benefits:

By following these practices, you can create more maintainable and robust React applications with cleaner state management.

Improving Component Structure and Reusability

  1. Handling Multiple Actions in Event Handlers

When dealing with events that require multiple actions, it's best to create a handler function that encompasses all the necessary logic. This approach keeps components simpler and more focused.

Example:

const handleAddTodo = (content) => {
  // Add todo logic
  // Update model state
  // Any other necessary actions
};
  1. Keeping Components "Dumb"

Avoid passing raw setter functions or complex logic to lower-level components. Instead, create handler functions that encapsulate the required logic and pass these functions as props.

  1. Centralizing State Management

Keep state and its update functions close together in the component tree. This makes it easier to manage and understand how the state is being modified.

Example:

const [todos, setTodos] = useState([]);
const handleAddTodo = (content) => { /* ... */ };
const handleEditTodo = (id, newContent) => { /* ... */ };
const handleDeleteTodo = (id) => { /* ... */ };
  1. Naming Conventions for Props

Follow conventions similar to native HTML or JSX events when naming props for custom components:

  1. TypeScript for Prop Typing

Use TypeScript to define prop types, which helps catch errors and provides better documentation:

type AddTodoFormProps = {
  onAddTodo: (content: string) => void;
};
  1. Reusable Components

When creating reusable components like buttons, keep them generic and customizable through props:

const Button = ({ type, onClick, children }) => (
  <button className={`base-style ${type}`} onClick={onClick}>
    {children}
  </button>
);

// Usage
<Button type="secondary" onClick={handleLogin}>Log In</Button>
<Button type="secondary" onClick={handleRegister}>Register</Button>
  1. Lifting Complexity

Move specific logic and state management to higher-level components, keeping lower-level components simple and reusable.

By following these practices, you can create more maintainable, reusable, and understandable React components. This approach allows for better separation of concerns and makes it easier to reason about your application's structure and behavior.

React UI Component Best Practices

Identifying Reusable Components

When building a React application, it's crucial to identify opportunities for component reuse. This approach offers several benefits:

  1. Consistency across the UI
  2. Easier maintenance and updates
  3. Improved code organization

Example: Reusable Button Component

In our app, we have three buttons that share similar markup, styling, and functionality:

By creating a single, reusable button component, we can:

Creating Components for Organization

Even when components aren't reusable, it's often beneficial to create separate components for organizational purposes. Examples include:

These components improve code readability and make the overall structure of the app easier to understand.

Best Practices for Component Creation

  1. Create components for reusable elements, even if only used twice
  2. Use components to organize markup and JSX, even if not reusable
  3. Balance between creating too many small components and keeping the main file readable

Avoiding Unnecessary Divs

When creating components, it's common to see unnecessary wrapper divs. Best practices include:

  1. Remove unnecessary wrapper divs
  2. Return the main element directly from the component
  3. Use React Fragments when multiple elements need to be returned without a wrapper

Handling Conditional Rendering

When dealing with conditional rendering, such as showing different buttons based on user authentication status, consider these approaches:

  1. Use ternary operators for simple conditions
  2. Extract complex conditions into separate components or functions
  3. Utilize React Fragments to return multiple elements without adding extra DOM nodes

By following these best practices, you can create more maintainable, efficient, and organized React applications.

Here's an improved and structured version of the text:

React Component Best Practices and Performance Optimization

Generalizing Components

When creating components, aim to make them more general and reusable:

Performance Optimization with Hooks

useMemo

Use useMemo for expensive calculations or to prevent unnecessary re-computations:

const completedTodosCount = useMemo(() => {
  return todos.filter(todo => todo.completed).length;
}, [todos]);

Benefits:

useCallback

Use useCallback for functions to prevent unnecessary recreations:

const handleAddTodo = useCallback((newTodo) => {
  setTodos(prevTodos => [...prevTodos, newTodo]);
}, []);

Benefits:

React.memo

Wrap components with React.memo to prevent unnecessary re-renders:

const MemoizedComponent = React.memo(MyComponent);

Use in conjunction with useMemo or useCallback for props to ensure maximum benefit.

State Management Best Practices

When updating state that depends on previous state, use the updater function format:

setTodos(prevTodos => [...prevTodos, newTodo]);
setCount(prevCount => prevCount + 1);

Benefits:

Future Considerations

Remember, mastering these concepts takes practice. Consider diving deeper into React courses or documentation for a more comprehensive understanding.

Improving React Component Structure and Reusability

  1. Creating Smaller, Specific Components

To improve code readability and maintainability, create smaller, more specific components for repetitive or complex elements. For example:

  1. Simplifying JavaScript Logic

For complex JavaScript logic, create utility functions to improve readability and reusability:

  1. Creating Reusable Button Components

To avoid duplicating markup for similar elements:

  1. Handling Complex Logic

For complex logic within components:

  1. Creating Custom Hooks

For reusable logic that includes React hooks:

  1. Best Practices for Reusability

Follow these guidelines for creating reusable code:

  1. Improving Initial State Loading

When using local storage with useState:

By applying these best practices, you can significantly improve the structure, readability, and reusability of your React components. Remember to handle TypeScript issues and refer to comprehensive courses for more in-depth coverage of these topics.

Improving React Component Design with TypeScript

  1. Using TypeScript for Props

TypeScript can significantly enhance your React development experience by providing type checking and intelligent code completion. Here's how to use TypeScript effectively with React props:

Benefits of using TypeScript:

  1. Keeping Components "Dumb"

A best practice in React is to keep components, especially reusable ones, as simple or "dumb" as possible. This improves reusability and maintainability.

Example: Status Bar Component

Instead of:

const StatusBar = ({ todos }) => {
  const width = `${(todos.filter(todo => todo.completed).length / todos.length) * 100}%`;
  return <div style={{ width }} />;
};

Prefer:

const StatusBar = ({ progressPercentage }) => {
  return <div style={{ width: `${progressPercentage}%` }} />;
};

Benefits:

  1. Handling Derived State

Calculate derived state close to where the original state is managed, rather than in child components.

Example:

const App = () => {
  const [todos, setTodos] = useState([]);
  const completedPercentage = (todos.filter(todo => todo.completed).length / todos.length) * 100;

  return (
    <div>
      <StatusBar progressPercentage={completedPercentage} />
      {/* Other components */}
    </div>
  );
};
  1. Avoiding Prop Drilling

Instead of passing state setters down through multiple levels of components, consider using context or state management libraries for complex state scenarios.

Example of what to avoid:

const App = () => {
  const [todos, setTodos] = useState([]);
  return <Sidebar setTodos={setTodos} />;
};

const Sidebar = ({ setTodos }) => {
  return <AddTodoForm setTodos={setTodos} />;
};

Better approach: Use React Context or state management libraries like Redux for managing global state and actions.

By following these best practices, you can create more maintainable, reusable, and scalable React applications.

Here's an improved and structured version of the transcription:

URL-based State Management in React

Benefits of URL-based State

  1. Shareable views: Users can copy and paste URLs to share specific states of the application.
  2. Bookmarkable: Users can save specific views for later access.
  3. Persistent state: The application can restore the exact view based on URL parameters.

Implementation Example

Best Practices

Understanding and Using the useEffect Hook

Basic Concept

Common Use Cases

  1. Synchronizing React state with external systems (e.g., localStorage)
  2. Fetching data from external sources

Example: Synchronizing with localStorage

useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);

Best Practices

  1. Separate concerns: Use different useEffect calls for different purposes
  2. Avoid multiple unrelated operations in a single useEffect

Incorrect Example:

useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
  document.addEventListener('keydown', handleEscapeKey);
}, [todos]);

Correct Example:

useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);

useEffect(() => {
  document.addEventListener('keydown', handleEscapeKey);
  return () => document.removeEventListener('keydown', handleEscapeKey);
}, []);

Data Fetching with useEffect

Example Implementation

useEffect(() => {
  const fetchJobItem = async () => {
    setIsLoading(true);
    const response = await fetch(`${BASE_API_URL}/jobs/${activeId}`);
    const data = await response.json();
    setJobData(data);
    setIsLoading(false);
  };

  fetchJobItem();
}, [activeId]);

Considerations

By structuring the content and improving readability, the key points of the transcription are now more clearly presented and easier to understand.

w3bdesign commented 1 month ago

React Component Refactoring Suggestions

1. Hero.component.tsx

Improvements:

  1. Use TypeScript interfaces for better type safety:

    interface Hero {
     text: string;
    }
    
    interface HeroProps {
     content: Hero[];
    }
  2. Utilize React.FC for functional components:

    const Hero: React.FC<HeroProps> = ({ content }) => { ... }
  3. Consider extracting the background image styles into a separate CSS file or use CSS modules for better separation of concerns.

  4. Use optional chaining and nullish coalescing for safer access to content:

    <h2>{content?.[0]?.text ?? "Hei!"}</h2>
  5. Consider creating separate components for each section of the hero (e.g., HeroTitle, HeroSubtitle) to improve readability and maintainability.

  6. Use React.memo to optimize performance for components that don't frequently update:

    export default React.memo(Hero);

2. Icons.component.tsx

Improvements:

  1. Move the AnimateIcons array outside the component to prevent unnecessary re-creation on each render:

    const AnimateIcons: IAnimateIcons[] = [
     { id: 0, Icon: FaReact, iconName: "React" },
     // ...
    ];
    
    const Icons: React.FC = () => { ... }
  2. Use React.memo to optimize performance:

    export default React.memo(Icons);
  3. Consider using a custom hook for icon data if it might be used elsewhere in the application.

3. IndexContent.component.tsx

Improvements:

  1. Separate the PortableText components into their own file for better organization.

  2. Use TypeScript interfaces for better type safety:

    interface SectionProps {
     text: IText[];
     title: string;
     id: string;
    }
    
    const Section: React.FC<SectionProps> = ({ text, title, id }) => { ... }
  3. Consider using React.memo for the Section component if it's likely to be re-rendered frequently with the same props.

  4. Use optional chaining when mapping over pageContent:

    {pageContent?.map((page) => <Section key={page.id} {...page} />)}

4. page.tsx

Improvements:

  1. Consider moving the Sanity query to a separate file or custom hook for better separation of concerns.

  2. Use TypeScript interfaces for the pageContent structure:

    interface PageContent {
     id: string;
     title: string;
     hero: Hero[];
     content: IContent[];
    }
  3. Add error handling for the Sanity fetch:

    try {
     const pageContent = await client.fetch(pageContentQuery);
     // ...
    } catch (error) {
     console.error("Failed to fetch page content:", error);
     // Handle error state
    }
  4. Consider using Suspense for a better loading experience:

    import { Suspense } from 'react';
    
    // ...
    
    <Suspense fallback={<LoadingSpinner />}>
     {pageContent.hero && <DynamicHero content={pageContent.hero} />}
    </Suspense>

General Recommendations:

  1. Implement proper error boundaries throughout the application.
  2. Consider using a state management solution like React Context or Redux for global state if the application grows more complex.
  3. Implement unit tests for components and integration tests for pages.
  4. Use constants for repeated string values, especially for data fetching queries.
  5. Implement proper accessibility attributes and ensure the application is fully accessible.
  6. Consider implementing code splitting for larger components to improve initial load time.
w3bdesign commented 4 weeks ago

single-responsibility principle (SRP)