what-is-it
What is Redux?​
Redux: Redux is a predictable state container for JavaScript applications. It provides a centralized way to manage and update the state of an application, making it easier to reason about data flow and changes.
Redux Building Blocks:
-
Store: The single source of truth. It represents the entire state of a Redux application. Only one store exists in a Redux application.
-
Actions: Plain JavaScript objects that represent information about what happened in the app. They have a type field, which describes the type of action being performed, and often contain additional data.
-
Reducers: Pure functions that take the current state and an action, then return a new state. They specify how the state changes in response to an action.
-
Middleware: Provides a third-party extension point between dispatching an action and the moment it reaches the reducer. Middleware is useful for logging, crash reporting, asynchronous API calls, etc.
-
Dispatch: The process by which actions are sent to the store and, subsequently, reducers. When an action is dispatched, Redux will update the state by calling the relevant reducer with the dispatched action.
-
Selectors: Functions that extract specific slices of the state. They can be used to derive data from the state, allowing components to get only the pieces of state they need.
The flow in a Redux application typically goes as follows: An action is dispatched, middleware processes the action (if any middleware is defined), the reducer computes the new state, and then the UI updates based on the new state.
Redux Store​
The Redux store is the heart of a Redux application. It's a centralized object that holds the entire state of the app. Here are its main characteristics and responsibilities:
-
Single Source of Truth: The Redux store contains the whole state tree of the application in a single JavaScript object. This means that when you want to understand the state of your app at any given point in time, you only need to look at this one object.
-
Read-Only State: The state stored in the Redux store is immutable. This means that you cannot change it directly. Instead, you express an intent to transform the state by dispatching actions.
-
State Manipulation Through Reducers: The only way to change the state inside the store is by dispatching an action, which is then handled by a reducer. Reducers are pure functions that take the current state and an action, and return a new state.
-
Consistency & Predictability: Because of the above principles, and the fact that all state transitions are explicit and centralized, it's easier to maintain, debug, and understand the application.
-
Middleware & Enhancers: The store has built-in capabilities to use middleware for tasks like logging, crash reporting, or handling asynchronous actions. Enhancers can further modify the store setup, allowing for more advanced functionalities like lazy-loading reducers.
In essence, the Redux store provides a predictable way to manage and update your app's state, making it easier to develop, debug, and maintain complex applications.
Redux Actons​
Certainly. For interview purposes, clarity and precision are key. Here's how you can explain Actions in Redux:
Redux Actions - Explanation for Interview:
Actions in Redux are payloads of information that send data from your application to the Redux store. They play a central role in the Redux data flow. Here's a breakdown of their characteristics and purpose:
-
Plain JavaScript Objects: At their core, actions are plain JavaScript objects.
-
Type Property: Every action must have a
type
property, which is typically defined as a string constant. Thistype
describes the kind of state change that should be performed. -
Payload: Actions can also carry a
payload
, which is the data or information that needs to be updated in the state. The payload is optional and can be structured however you like. -
Creator Functions: While actions are objects, it's a common practice to use functions, called "action creators", to create these objects. This approach provides a consistent and reusable way to generate actions.
-
Dispatching Actions: Actions are dispatched using the
dispatch
function provided by Redux. Once an action is dispatched, it's sent to the reducers to process and produce a new state. -
Intent Expressers: Think of actions as expressing an "intent" to change the state. They describe what happened in the app, but not how the state changes. The "how" part is handled by reducers.
Best Way to Explain Redux Actions:
-
Analogy: Actions are like "commands" given to a system. If you're playing a video game and press a button to make the character jump, that button press is akin to dispatching an action. The game's code, which determines how the character jumps, is like the reducer.
-
Example:
// Action Type
const ADD_TODO = 'ADD_TODO';
// Action Creator
function addTodo(text) {
return {
type: ADD_TODO,
payload: text
};
}
// Dispatching the Action
dispatch(addTodo('Learn Redux'));
In summary, actions in Redux are essential signals that notify the store about the changes the application intends to make to its state. They set the stage for the reducers to process these changes.
Reducers​
In Redux, a reducer is a pure function that takes the current state of the application and an action as arguments, and returns a new state. The term "pure function" means that given the same input, the reducer will always return the same output without producing any side effects.
Here's how to explain reducers in Redux during an interview:
-
State Managers: Reducers are responsible for managing changes to the application’s state. They do this by specifying how the state should change in response to an action.
-
Pure Functions: Reducers must be pure functions. This means they should not mutate the existing state, nor should they have side effects or make asynchronous calls. Instead, they return a new object that represents the updated state.
-
(previousState, action) => newState: The signature of a reducer function is
(previousState, action) => newState
. It takes the previous state and an action, and returns the next state. -
Action Handlers: Reducers listen for dispatched actions identified by the action’s
type
property. They interpret the action and decide how the state should change. -
Immutable Updates: When updating the state, reducers produce a new object or array rather than modifying the existing one. This is typically done using operations like the spread operator or utilities from libraries like Immer.
-
Composition: In large applications, multiple reducers can be combined with
combineReducers()
to manage different parts of the state tree, making the reducers more manageable and the application state more organized.
Best Way to Explain Redux Reducers:
-
Analogy: If actions are like the requests you make to a chef in a kitchen, then reducers are the chefs themselves. They take your request (the action) and the current state of the meal (the state), and they produce the next version of the meal (the new state).
-
Example: Here’s a simple example of a reducer that handles adding a todo item:
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload];
default:
return state;
}
}
In this example, the todosReducer
checks the action type. If it's ADD_TODO
, it returns a new array with the new todo item appended. For any other action, it returns the current state unchanged.
In essence, reducers in Redux determine how the state of an application changes in response to actions sent to the store. Their predictable behavior and pure function characteristics make them a cornerstone of the Redux architecture.
Selectors​
Selectors in Redux are functions that allow you to select, derive, and compute data from the Redux store. They're used for selecting pieces of state and can also be used to compute derived data, allowing the store to keep minimal state.
Redux Selectors - Brief Interview Explanation:
-
State Readers: Selectors are functions that read or derive values from the Redux store state. They encapsulate the state shape, so components don't need to know about it.
-
Performance Optimization: By retrieving only the necessary data and using techniques like memoization (e.g., with
reselect
library), selectors can prevent unnecessary re-renders and improve performance. -
Code Organization: Selectors help to keep your Redux-related code organized by abstracting the state access logic away from components and keeping it with the reducers.
-
Reusability: You can reuse selectors across different components, which promotes DRY (Don't Repeat Yourself) principles and simplifies maintenance.
Example Usage in a Component:
const myData = useSelector(selectMyData);
In this example, selectMyData
is a selector function that knows how to extract myData
from the Redux store. The useSelector
hook then uses this selector to provide the data to your component.
Selectors are a powerful pattern for accessing and computing derived state, leading to more maintainable and efficient applications.
Dispatch​
In Redux, dispatch
is a function that sends actions to the Redux store. Actions are the only way to trigger a state change in the store. When an action is dispatched, the store's reducers respond to this action and return a new state based on the action's type and payload.
Here's a concise explanation suitable for an interview:
-
Action Sender: The dispatch function is used to send actions to the Redux store to trigger a state change.
-
Trigger for Reducers: When an action is dispatched, the Redux store's reducers handle the action, resulting in a new state.
-
Middleware Interaction: Dispatch can be extended with middleware, allowing for more complex behavior like asynchronous actions or logging.
-
Core Redux API: It is a part of the Redux store API, made available by the
useDispatch
hook orconnect
function in React-Redux bindings.
Usage Example:
dispatch({ type: 'ADD_TODO', payload: 'Learn Redux' });
In this example, the dispatch
function is used to send an ADD_TODO
action to the store, indicating that we want to add a new todo item with the text 'Learn Redux'. The store's reducer will then handle this action to update the state accordingly.
Middleware​
Middleware in Redux is a powerful way to extend Redux with additional functionality, such as handling asynchronous actions or logging every action dispatched to the store. Here's how to use middleware in Redux:
-
Create Middleware: Middleware is a higher-order function that takes
dispatch
andgetState
as arguments and returns a function that takesnext
(the next middleware in the chain), which in turn returns a function that takes anaction
. The innermost function has access todispatch
andgetState
, thenext
middleware, and theaction
, allowing it to run any code it wants when an action is dispatched.Here is a template for a middleware function:
const myMiddleware = store => next => action => {
// Perform middleware actions here
// For example, logging the action:
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
}; -
Apply Middleware: To apply middleware to your Redux store, you use the
applyMiddleware
function from Redux in conjunction withcreateStore
.Example of applying middleware:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'; // an example of asynchronous middleware
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(
thunk, // You can apply multiple middlewares like this
myMiddleware
)
); -
Use Middleware: Once middleware is applied to the store, it will run its code every time an action is dispatched. For example, if you're using
redux-thunk
, it allows you to write action creators that return a function instead of an action object. This function can then be used to dispatch actions asynchronously or after certain conditions are met.Using
redux-thunk
for asynchronous actions:const fetchUserData = userId => {
return (dispatch, getState) => {
dispatch({ type: 'LOADING_USER_DATA' });
fetch(`/users/${userId}`)
.then(response => response.json())
.then(data => dispatch({ type: 'USER_DATA_RECEIVED', payload: data }))
.catch(error => dispatch({ type: 'USER_DATA_FETCH_FAILED', error }));
};
}; -
Compose with DevTools: If you're using Redux DevTools, you can compose middleware with DevTools like so:
import { composeWithDevTools } from 'redux-devtools-extension';
const store = createStore(
rootReducer,
composeWithDevTools(
applyMiddleware(thunk, myMiddleware)
// other store enhancers if any
)
);
Middleware can transform, delay, ignore, or otherwise interpret actions or async actions in any way you want, before they reach the reducer. It's a central piece of most Redux applications, especially for handling side effects.
Thunk API​
In Redux, the term "Thunk" is commonly associated with the Redux Thunk middleware, which allows you to write action creators that return a function instead of an action object. This function, often referred to as a thunk, can be used to delay the dispatch of an action or to dispatch only if certain conditions are met. The functions you write can perform asynchronous operations, access the current state of the store, and dispatch actions as needed.
The ThunkAPI in Redux consists of two main arguments:
dispatch
: The store's dispatch function, which you can use to dispatch actions at any time after some asynchronous operations.getState
: A function that returns the current state of the Redux store, allowing your action creator to access the current state.
Here's how to use Redux Thunk:
-
Install Redux Thunk: To use Redux Thunk, you first need to install it and apply it as a middleware to your Redux store.
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
); -
Create a Thunk Action Creator: A thunk action creator is a function that returns a thunk function. The thunk function takes
dispatch
andgetState
as arguments.// Thunk action creator
const fetchUserData = (userId) => {
// The thunk function
return (dispatch, getState) => {
// Start by dispatching an action
dispatch({ type: 'USER_FETCH_REQUESTED' });
// Perform the async operation
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
// Dispatch an action when data is received
dispatch({ type: 'USER_FETCH_SUCCEEDED', payload: data });
})
.catch(error => {
// Dispatch an action if there's an error
dispatch({ type: 'USER_FETCH_FAILED', error });
});
};
}; -
Dispatch the Thunk Action: You dispatch the thunk action creator just like you would dispatch a regular action creator. The Redux Thunk middleware takes care of invoking the thunk function with
dispatch
andgetState
.store.dispatch(fetchUserData(123));
The thunk function can also dispatch other thunk actions, allowing for complex asynchronous logic to be written in a structured way. This ability to write functions that can dispatch actions asynchronously and conditionally is powerful, as it enables you to handle complex logic within your action creators.
Call Multiple endpoints using Thunk API​
When you need to call multiple endpoints using the Thunk API in a React application, you can chain your asynchronous calls within a single thunk action creator. Here's a step-by-step process on how to achieve this:
-
Define the Thunk Action Creator: You'll create a thunk action creator that dispatches actions to manage the loading state, handle the data when the promises resolve, and handle errors if any of the promises reject.
-
Make Asynchronous Calls: Use
Promise.all
to call multiple endpoints if the calls can be made concurrently without waiting for one another to finish. If the calls need to be made sequentially (where one call depends on the data from the previous), you would chain the promises accordingly. -
Dispatch Actions as Needed: Dispatch actions to your store to reflect the state of your requests (e.g., loading, success, error).
Here is an example of a thunk action creator making concurrent API calls:
// Import the necessary functions from Redux and your action types
import { createAction } from '@reduxjs/toolkit';
// Action creators
const fetchStart = createAction('FETCH_START');
const fetchSuccess = createAction('FETCH_SUCCESS');
const fetchError = createAction('FETCH_ERROR');
// Thunk action creator
const fetchMultipleData = () => {
return async (dispatch, getState) => {
try {
// Dispatch an action to indicate the start of the fetch process
dispatch(fetchStart());
// Define the endpoints
const endpoints = [
'/api/endpoint1',
'/api/endpoint2',
'/api/endpoint3',
// Add as many endpoints as you need
];
// Use Promise.all to fetch data from multiple endpoints concurrently
const data = await Promise.all(
endpoints.map((url) => fetch(url).then((res) => res.json()))
);
// Dispatch an action with the data once all promises are resolved
dispatch(fetchSuccess(data));
} catch (error) {
// Dispatch an error action if any of the promises reject
dispatch(fetchError(error.toString()));
}
};
};
And here's how you might handle sequential API calls:
// Thunk action creator for sequential API calls
const fetchSequentialData = () => {
return async (dispatch) => {
try {
dispatch(fetchStart());
// Fetch the first data set
const firstResponse = await fetch('/api/firstEndpoint');
const firstData = await firstResponse.json();
// Maybe the second call depends on data from the first
const secondResponse = await fetch(`/api/secondEndpoint?param=${firstData.someValue}`);
const secondData = await secondResponse.json();
// Continue with as many sequential calls as needed
// ...
// Once all calls are done, dispatch a success action with all data
dispatch(fetchSuccess({ firstData, secondData }));
} catch (error) {
dispatch(fetchError(error.toString()));
}
};
};
To use these thunk action creators, you would dispatch them from your React components in response to user interactions or lifecycle events:
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fetchMultipleData, fetchSequentialData } from './actions/thunks';
const MyComponent = () => {
const dispatch = useDispatch();
useEffect(() => {
// Dispatch the thunk action creator to make the API calls
dispatch(fetchMultipleData());
// Or for sequential calls
// dispatch(fetchSequentialData());
}, [dispatch]);
// ... render your component
};
In both cases, you need to make sure your Redux store is set up with the Redux Thunk middleware, and you have the appropriate reducers to handle the dispatched actions for updating the store's state.
mapStateToProps Vs mapDispatchToProps​
In Redux, mapStateToProps
and mapDispatchToProps
are used in conjunction with the connect
higher-order component to link React components with the Redux store. These functions define how to take the current Redux store state and the dispatch
method and turn them into props that can be passed to your React components. However, the terms "merge state to props" and "merge dispatch to props" are not standard Redux terminology; it seems you're referring to the process of merging state and dispatched actions into the component's props. Here's how they generally work:
mapStateToProps
: This function allows you to specify which slice of the Redux state you want to pass to your React component's props. When the Redux store updates,mapStateToProps
will be called and its result will be merged into your component’s props. The function receives the entire store state, and should return an object where each key/value pair is a prop you want to pass to your component.
function mapStateToProps(state) {
return {
todos: state.todos,
};
}
mapDispatchToProps
: This function lets you create functions that dispatch actions to the Redux store. The functions that it returns are merged into your component’s props. You can either define it as a function, where you manually create props that calldispatch
, or you can define it as an object where each field is an action creator.
function mapDispatchToProps(dispatch) {
return {
addTodo: (content) => dispatch(addTodoAction(content)),
};
}
// Or as an object:
const mapDispatchToProps = {
addTodo: addTodoAction,
};
When you connect your component to the Redux store using connect(mapStateToProps, mapDispatchToProps)
, the connect
function does the merging for you. It combines the objects returned from mapStateToProps
and mapDispatchToProps
into a single props object, which is then passed to your component.
If you need to compute derived data from the state and the props coming into the component, you might also make use of mergeProps
, which is a third optional argument to connect
. It takes the results of mapStateToProps
and mapDispatchToProps
along with the component's own props and merges them however you see fit.
function mergeProps(stateProps, dispatchProps, ownProps) {
return { ...ownProps, ...stateProps, ...dispatchProps };
}
connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent);
In newer versions of Redux, the hooks API (useSelector
and useDispatch
) is often used instead of connect
, mapStateToProps, and mapDispatchToProps, providing a more straightforward way to interact with the Redux store in functional components.
Memoization​
Memoization in React is an optimization technique that helps to avoid expensive calculations or re-rendering of components when it's not necessary. It involves caching the results of a function call and returning the cached result when the same inputs occur again.
React provides several built-in ways to apply memoization:
React.memo
: This is a higher-order component that memoizes functional components. It performs a shallow comparison of the previous and new props and only re-renders the component if the props have changed.
const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});
useMemo
: This hook is used to memoize values. You pass it a function that returns a value and an array of dependencies.useMemo
will only recompute the memoized value when one of the dependencies has changed. This is useful for expensive calculations that you don’t want to run on every render.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useCallback
: Similar touseMemo
, but it’s used to memoize functions. This is handy when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g., when wrapped withReact.memo
).
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Memoization can greatly improve performance, especially for components with complex rendering or expensive calculations. However, it's important to use it judiciously, as inappropriate use can lead to memory overhead and can potentially make your application slower if used in scenarios where invalidations are frequent.
FLUX Architecture​
Flux is an architectural pattern introduced by Facebook for building client-side web applications. It is not a library or a framework but a kind of architecture that complements React's composable view components by utilizing a unidirectional data flow. It's more of a pattern rather than a formal framework, and you can start using Flux immediately without a lot of new code.
Flux applications have three major parts: the dispatcher, the stores, and the views (React components). Here’s a brief overview of each part:
-
Dispatcher: The dispatcher is the central hub that manages all data flow in a Flux application. It is essentially a registry of callbacks into the stores and has no real intelligence of its own; it's just a mechanism for distributing the actions to the stores.
-
Stores: Stores contain the application's state and logic. Their role is somewhat similar to a model in a traditional MVC (Model-View-Controller) system, but they manage the state of many objects — they do not represent a single record of data like ORM models do. When a store changes, it broadcasts an event to signal that its state has changed, so views can respond by updating themselves.
-
Views: React components grab the state from the stores and pass it down via props to child components. When the user interacts with the React components, the components propagate actions (plain objects describing what happened) back to the dispatcher, which then calls the appropriate store callbacks.
-
Actions: Actions are simple objects that contain a type identifier and some data. They represent the 'what happened' part of the system. Actions are sent to the dispatcher in response to user interactions like clicking a button, submitting a form, etc., and they are the only way to get data into the stores.
Flux’s single-direction flow looks like this:
- Actions — Helper methods that facilitate passing data to the Dispatcher
- Dispatcher — Receives actions and broadcasts payloads to registered callbacks
- Stores — Containers for application state & logic that have callbacks registered to the dispatcher
- Views — React components that listen for changes from the Stores and re-render themselves
The idea behind Flux is that it's easier to reason about your application if data flows in a single direction. This unidirectional flow ensures that your views remain consistent with the state of your application, making the behavior of your app more predictable and easier to understand.
Flux has inspired several state management libraries, the most notable being Redux, which has gained substantial popularity due to its simplicity, small size, and compatibility with React.