Functional programming is a paradigm that emphasizes immutability, pure functions, and declarative code. React naturally aligns with many functional programming principles, making it an excellent framework for applying these concepts.
Pure functions are the foundation of functional programming. They always return the same output for the same input and have no side effects.
// Pure component - always renders the same output for the same props
const UserCard = ({ name, email }) => (
<div className="user-card">
<h3>{name}</h3>
<p>{email}</p>
</div>
);
// Impure component - depends on external state
const ImpureUserCard = ({ name }) => (
<div className="user-card">
<h3>{name}</h3>
<p>{Math.random()}</p> {/* Side effect - not predictable */}
</div>
);
React forms can be managed in two main ways: controlled and uncontrolled components. Understanding both approaches helps you choose the right one for your use case.
Controlled components are fully managed by React state. You pass the value and change handler as props, making the component predictable and testable.
// Controlled input component
const ControlledInput = ({ value, onChange, placeholder }) => (
<input
type="text"
value={value}
onChange={onChange}
placeholder={placeholder}
/>
);
// Usage
const App = () => {
const [inputValue, setInputValue] = useState('');
return (
<ControlledInput
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Enter text..."
/>
);
};
Uncontrolled components manage their own state internally, using refs to access values. They are less predictable but sometimes useful for simple use cases or integrating with non-React code.
// Uncontrolled input component
const UncontrolledInput = ({ placeholder }) => {
const inputRef = useRef();
const handleButtonClick = () => {
alert(inputRef.current.value);
};
return (
<>
<input type="text" ref={inputRef} placeholder={placeholder} />
<button onClick={handleButtonClick}>Show Value</button>
</>
);
};
// Usage
const App = () => <UncontrolledInput placeholder="Enter text..." />;
A higher-order component is an example of functional programming because they're functions that return other functions, which is an application of first-class functions.
// HOC that adds loading functionality
const withLoading = (WrappedComponent) => {
return ({ isLoading, ...props }) => {
if (isLoading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
};
// Usage
const UserList = ({ users }) => (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
const UserListWithLoading = withLoading(UserList);
React Hooks enable function composition, allowing us to build complex logic from simple, reusable functions.
// Custom hooks as composable functions
const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return { count, increment, decrement, reset };
};
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
});
const setStoredValue = useCallback(
(newValue) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
},
[key]
);
return [value, setStoredValue];
};
// Composing hooks
const Counter = () => {
const { count, increment, decrement, reset } = useCounter(0);
const [persistedCount, setPersistedCount] = useLocalStorage('count', 0);
useEffect(() => {
setPersistedCount(count);
}, [count, setPersistedCount]);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
};
Functional programming emphasizes immutability. In React, we should never mutate state directly but create new objects/arrays.
// ❌ Mutating state directly
const TodoList = () => {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
todos.push({ id: Date.now(), text, completed: false }); // Mutation!
setTodos(todos);
};
// This won't trigger re-render because the array reference hasn't changed
};
// ✅ Immutable state updates
const TodoList = () => {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos((prevTodos) => [
...prevTodos,
{ id: Date.now(), text, completed: false },
]);
};
const toggleTodo = (id) => {
setTodos((prevTodos) =>
prevTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const removeTodo = (id) => {
setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
};
return (
<div>
{todos.map((todo) => (
<div key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => toggleTodo(todo.id)}>Toggle</button>
<button onClick={() => removeTodo(todo.id)}>Remove</button>
</div>
))}
</div>
);
};
useReducer
follows functional programming principles by using pure reducer
functions to manage complex state.
// Pure reducer function
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload,
completed: false,
},
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'SET_FILTER':
return {
...state,
filter: action.payload,
};
default:
return state;
}
};
const TodoApp = () => {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
});
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
// Component JSX...
};
React encourages declarative programming - describing what the UI should look like rather than how to achieve it.
// Imperative approach (vanilla JavaScript)
function createUserList(users) {
const ul = document.createElement('ul');
for (let i = 0; i < users.length; i++) {
const li = document.createElement('li');
li.textContent = users[i].name;
ul.appendChild(li);
}
document.body.appendChild(ul);
}
// Declarative approach (React)
const UserList = ({ users }) => (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
React.memo
and
other techniquesFunctional programming in React isn't just about using functions - it's about embracing immutability, composition, and declarative patterns. By following these principles, you'll write more maintainable, testable, and predictable React applications.
The combination of React's component model with functional programming concepts creates a powerful paradigm for building modern user interfaces that are both performant and easy to reason about.
Let me know what you think