React Architecture

React is a popular JavaScript library for building user interfaces, particularly single-page applications, using a component-based architecture. Function components, introduced with React 16.8 and enhanced with Hooks, have become the standard way to build React applications due to their simplicity and flexibility. Below, I’ll explain the React architecture with a focus on function components, covering their structure, lifecycle, state management, and best practices, along with examples.


1. Overview of React Architecture

React’s architecture revolves around components, which are reusable, self-contained pieces of UI logic. Function components are JavaScript functions that return JSX (a syntax extension resembling HTML) to describe the UI. They are stateless by default but can manage state and side effects using React Hooks.

Key Principles

  • Declarative: React allows developers to describe what the UI should look like based on state, and React handles rendering updates efficiently.
  • Component-Based: UI is broken into independent components that encapsulate their own logic, styling, and rendering.
  • Unidirectional Data Flow: Data flows from parent to child components via props, ensuring predictable state management.
  • Virtual DOM: React maintains a lightweight in-memory representation of the DOM, minimizing direct DOM manipulations for performance.

Function Components vs. Class Components

  • Function Components: Lightweight, simpler syntax, no this binding, and use Hooks for state and lifecycle management.
  • Class Components: Older approach, more verbose, use class methods and lifecycle methods (e.g., componentDidMount).

Function components are now preferred due to their conciseness and the power of Hooks, which eliminate the need for class-based complexities.


2. Anatomy of a Function Component

A function component is a JavaScript function that accepts props as an argument and returns JSX. Here’s a basic example:

import React from 'react';

function Welcome(props) {
  return <h1>Hello, {props.name}!</h1>;
}

export default Welcome;

Or using an arrow function:

const Welcome = ({ name }) => <h1>Hello, {name}!</h1>;
export default Welcome;

Key Features

  • Props: Immutable inputs passed from parent components to customize rendering.
  • JSX: Syntactic sugar for React.createElement, used to describe the UI.
  • Hooks: Special functions (e.g., useState, useEffect) that add state and lifecycle features to function components.

3. React Hooks and Function Components

Hooks are functions that let you “hook into” React state and lifecycle features. They are the backbone of modern function components.

Core Hooks

  1. useState: Manages state in function components.
   import React, { useState } from 'react';

   const Counter = () => {
     const [count, setCount] = useState(0);

     return (
       <div>
         <p>Count: {count}</p>
         <button onClick={() => setCount(count + 1)}>Increment</button>
       </div>
     );
   };
  • How it works: useState returns a state variable (count) and a setter function (setCount). Calling setCount triggers a re-render with the new state.
  1. useEffect: Handles side effects (e.g., data fetching, subscriptions).
   import React, { useState, useEffect } from 'react';

   const DataFetcher = () => {
     const [data, setData] = useState(null);

     useEffect(() => {
       fetch('https://api.example.com/data')
         .then((response) => response.json())
         .then((result) => setData(result));

       // Cleanup function (runs before unmount or next effect)
       return () => console.log('Cleanup');
     }, []); // Empty dependency array means run once on mount

     return <div>{data ? data.name : 'Loading...'}</div>;
   };
  • How it works: useEffect runs after render. The dependency array controls when it re-runs. The cleanup function prevents memory leaks.
  1. useContext: Accesses context for shared data (e.g., theme, auth).
   import React, { useContext } from 'react';
   const ThemeContext = React.createContext('light');

   const ThemedComponent = () => {
     const theme = useContext(ThemeContext);
     return <div>Current theme: {theme}</div>;
   };

Other Useful Hooks

  • useReducer: Manages complex state logic, similar to Redux.
  • useRef: Persists values across renders (e.g., DOM references).
  • useMemo: Memoizes expensive computations.
  • useCallback: Memoizes functions to prevent unnecessary re-creations.
  • Custom Hooks: Encapsulate reusable logic (e.g., useFetch for API calls).

4. Lifecycle in Function Components

Unlike class components, which have explicit lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount), function components manage lifecycles using useEffect.

Lifecycle Phases

Mount:

  • Component renders for the first time.
  • Use useEffect with an empty dependency array:
    jsx useEffect(() => { console.log('Component mounted'); return () => console.log('Component unmounted'); }, []);

Update:

  • Component re-renders due to state or prop changes.
  • Use useEffect with dependencies:
    jsx useEffect(() => { console.log('Prop or state changed'); }, [prop, state]);

Unmount:

  • Component is removed from the DOM.
  • Use the cleanup function in useEffect:
    jsx useEffect(() => { const timer = setInterval(() => console.log('Tick'), 1000); return () => clearInterval(timer); // Cleanup on unmount }, []);

5. State Management in Function Components

State management in function components is handled primarily with useState and useReducer.

  • Simple State with useState:
  const Toggle = () => {
    const [isOn, setIsOn] = useState(false);
    return (
      <button onClick={() => setIsOn(!isOn)}>
        {isOn ? 'On' : 'Off'}
      </button>
    );
  };
  • Complex State with useReducer:
  const initialState = { count: 0 };

  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return { count: state.count + 1 };
      case 'decrement':
        return { count: state.count - 1 };
      default:
        return state;
    }
  }

  const Counter = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
      <div>
        <p>Count: {state.count}</p>
        <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
        <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      </div>
    );
  };
  • External State Management:
    For complex apps, libraries like Redux, Zustand, or Recoil can be used with function components. For example, in an MFE architecture (as discussed previously), a shared Zustand store can manage state across MFEs.

6. Integration with Micro Frontends (MFEs)

Function components are ideal for MFE architectures because they are lightweight and modular. Here’s how they integrate with the communication methods:

Example: Function Component in an MFE

  • MFE 1 (mfe1/src/RemoteComponent.jsx):
  import React, { useState } from 'react';
  import { useSharedStore } from 'mfe1/SharedStore'; // Shared Zustand store

  const RemoteComponent = () => {
    const [localState, setLocalState] = useState('');
    const { setSharedData } = useSharedStore();

    const handleSend = () => {
      setSharedData(localState); // Update shared state
      window.dispatchEvent(new CustomEvent('mfe1.message', { detail: { message: localState } })); // Custom event
    };

    return (
      <div>
        <h2>MFE 1</h2>
        <input
          type="text"
          value={localState}
          onChange={(e) => setLocalState(e.target.value)}
        />
        <button onClick={handleSend}>Send to MFE 2</button>
      </div>
    );
  };

  export default RemoteComponent;
  • MFE 2 (mfe2/src/RemoteComponent.jsx):
  import React, { useState, useEffect } from 'react';
  import { useSharedStore } from 'mfe1/SharedStore';

  const RemoteComponent = () => {
    const { sharedData } = useSharedStore();
    const [eventMessage, setEventMessage] = useState('');

    useEffect(() => {
      const handleMessage = (event) => {
        setEventMessage(event.detail.message);
      };

      window.addEventListener('mfe1.message', handleMessage);
      return () => window.removeEventListener('mfe1.message', handleMessage);
    }, []);

    return (
      <div>
        <h2>MFE 2</h2>
        <p>Shared Store Data: {sharedData || 'No data'}</p>
        <p>Event Data: {eventMessage || 'No event data'}</p>
      </div>
    );
  };

  export default RemoteComponent;
  • Host App (host-app/src/App.jsx):
  import React from 'react';
  import MFE1 from 'mfe1/RemoteComponent';
  import MFE2 from 'mfe2/RemoteComponent';

  const App = () => (
    <div>
      <h1>Host Application</h1>
      <MFE1 />
      <MFE2 />
    </div>
  );

  export default App;

MFE Communication with Function Components

  • Custom Events: Use useEffect to listen for window events.
  • Shared State: Use useState or useReducer with a shared store (e.g., Zustand).
  • Props Passing: Pass callbacks and data via props from the host.
  • URL-based: Use react-router-dom hooks (useHistory, useLocation).
  • postMessage: Handle cross-origin communication with useEffect.
  • Storage: Monitor localStorage changes with useEffect.
  • Pub/Sub: Subscribe/publish with useEffect and libraries like PubSubJS.

Module Federation Setup

  • Expose function components via Webpack Module Federation (exposes in webpack.config.js).
  • Share dependencies (e.g., react, react-dom) as singletons to avoid duplication.
  • Use lazy loading with React.lazy and Suspense for dynamic MFE imports:
  const MFE1 = React.lazy(() => import('mfe1/RemoteComponent'));
  const App = () => (
    <Suspense fallback={<div>Loading...</div>}>
      <MFE1 />
    </Suspense>
  );

7. Rendering and Reconciliation

React’s rendering process in function components involves:

  1. Rendering: The function runs, returning JSX.
  2. Reconciliation: React compares the new Virtual DOM with the previous one using the Diffing Algorithm.
  3. Updating: Only changed DOM nodes are updated, leveraging the Virtual DOM for efficiency.

Optimization Techniques

  • useMemo: Prevent expensive calculations:
  const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useCallback: Prevent function re-creation:
  const memoizedCallback = useCallback(() => {
    doSomething(a, b);
  }, [a, b]);
  • React.memo: Prevent unnecessary re-renders of components:
  const MyComponent = React.memo(({ prop }) => <div>{prop}</div>);

8. Best Practices for Function Components

  • Keep Components Small: Break down complex UIs into smaller, reusable components.
  • Use Hooks Judiciously: Avoid overuse of useEffect for logic that can be handled declaratively.
  • Type Safety: Use TypeScript for props and state to catch errors early.
  • Avoid Inline Functions: Use useCallback for event handlers passed to children.
  • Clean Up Effects: Always return cleanup functions in useEffect to prevent memory leaks.
  • Consistent Naming: Prefix custom hooks with use (e.g., useFetch).
  • Error Boundaries: Wrap components in error boundaries (requires class components or libraries like react-error-boundary).

9. Example: Complete Function Component in an MFE

Here’s a comprehensive example combining state, effects, and MFE communication:

// mfe1/src/RemoteComponent.jsx
import React, { useState, useEffect, useCallback } from 'react';
import { useSharedStore } from 'mfe1/SharedStore';

const RemoteComponent = () => {
  const [input, setInput] = useState('');
  const { sharedData, setSharedData } = useSharedStore();

  // Send custom event
  const sendEvent = useCallback(() => {
    window.dispatchEvent(new CustomEvent('mfe1.message', { detail: { message: input } }));
  }, [input]);

  // Update shared store and send event
  const handleSend = () => {
    setSharedData(input);
    sendEvent();
  };

  // Log mount/unmount
  useEffect(() => {
    console.log('MFE 1 mounted');
    return () => console.log('MFE 1 unmounted');
  }, []);

  return (
    <div>
      <h2>MFE 1</h2>
      <input
        type="text"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter message"
      />
      <button onClick={handleSend}>Send to MFE 2</button>
      <p>Current Shared Data: {sharedData}</p>
    </div>
  );
};

export default React.memo(RemoteComponent);

10. React Architecture in Context of MFEs

In an MFE architecture:

  • Modularity: Each MFE is a function component or a collection of components, exposed via Module Federation.
  • Isolation: Function components encapsulate their state and logic, reducing conflicts between MFEs.
  • Communication: Use Hooks (useEffect, useState) to implement communication methods (e.g., events, shared state).
  • Performance: Optimize with React.memo, useMemo, and useCallback to minimize re-renders in distributed MFEs.
  • Scalability: Function components are lightweight, making them ideal for independently deployable MFEs.

11. Limitations and Considerations

  • Learning Curve: Hooks require understanding their rules (e.g., only call Hooks at the top level).
  • Overuse of Effects: Can lead to complex logic; prefer declarative solutions when possible.
  • MFE Challenges: Shared dependencies and communication contracts must be carefully managed to avoid version mismatches or runtime errors.
  • Debugging: Use React DevTools to inspect component trees and state.

Tags: No tags

Add a Comment

Your email address will not be published. Required fields are marked *