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
thisbinding, 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
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:
useStatereturns a state variable (count) and a setter function (setCount). CallingsetCounttriggers a re-render with the new state.
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:
useEffectruns after render. The dependency array controls when it re-runs. The cleanup function prevents memory leaks.
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.,
useFetchfor 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
useEffectwith 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
useEffectwith 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
useEffectto listen forwindowevents. - Shared State: Use
useStateoruseReducerwith a shared store (e.g., Zustand). - Props Passing: Pass callbacks and data via props from the host.
- URL-based: Use
react-router-domhooks (useHistory,useLocation). - postMessage: Handle cross-origin communication with
useEffect. - Storage: Monitor
localStoragechanges withuseEffect. - Pub/Sub: Subscribe/publish with
useEffectand libraries like PubSubJS.
Module Federation Setup
- Expose function components via Webpack Module Federation (
exposesinwebpack.config.js). - Share dependencies (e.g.,
react,react-dom) as singletons to avoid duplication. - Use lazy loading with
React.lazyandSuspensefor 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:
- Rendering: The function runs, returning JSX.
- Reconciliation: React compares the new Virtual DOM with the previous one using the Diffing Algorithm.
- 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
useEffectfor logic that can be handled declaratively. - Type Safety: Use TypeScript for props and state to catch errors early.
- Avoid Inline Functions: Use
useCallbackfor event handlers passed to children. - Clean Up Effects: Always return cleanup functions in
useEffectto 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, anduseCallbackto 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.
