I can't quite figure out the optimal way to use useReducer hook for data management. My primary goal is to reduce (heh) the boilerplate to minimum and maintain code readability, while using the optimal approach in terms of performance and preventing unnecessary re-renders.
The setup
I have created a simplified example of my app, basically it's a <List /> component - a list of items with possibility to select them, and a <Controls /> component which can switch item groups and reload the data set.
List.js
import React, { memo } from "react";
const List = ({ items, selected, selectItem, deselectItem }) => {
console.log("<List /> render");
return (
<ul className="List">
{items.map(({ id, name }) => (
<li key={`item-${name.toLowerCase()}`}>
<label>
<input
type="checkbox"
checked={selected.includes(id)}
onChange={(e) =>
e.target.checked ? selectItem(id) : deselectItem(id)
}
/>
{name}
</label>
</li>
))}
</ul>
);
};
export default memo(List);
Controls.js
import React, { memo } from "react";
import { ItemGroups } from "./constants";
const Controls = ({ group, setGroup, fetchItems }) => {
console.log("<Controls /> render");
return (
<div className="Controls">
<label>
Select group
<select value={group} onChange={(e) => setGroup(e.target.value)}>
<option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
<option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
</select>
</label>
<button onClick={() => fetchItems(group)}>Reload data</button>
</div>
);
};
export default memo(Controls);
App.js
import React, { useEffect, useReducer } from "react";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
import "./styles.css";
const itemsReducer = (state, action) => {
const { type, payload } = action;
console.log(`reducer action "${type}" dispatched`);
switch (type) {
case FETCH_START:
return {
...state,
isLoading: true
};
case FETCH_SUCCESS:
return {
...state,
items: payload.items,
isLoading: false
};
case SET_GROUP:
return {
...state,
selected: state.selected.length ? [] : state.selected,
group: payload.group
};
case SELECT_ITEM:
return {
...state,
selected: [...state.selected, payload.id]
};
case DESELECT_ITEM:
return {
...state,
selected: state.selected.filter((id) => id !== payload.id)
};
default:
throw new Error("Unknown action type in items reducer");
}
};
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
const fetchItems = (group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
const setGroup = (group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
};
const selectItem = (id) => {
dispatch({
type: SELECT_ITEM,
payload: { id }
});
};
const deselectItem = (id) => {
dispatch({
type: DESELECT_ITEM,
payload: { id }
});
};
useEffect(() => {
console.log("use effect on group change");
fetchItems(group);
}, [group]);
console.log("<App /> render");
return (
<div className="App">
<Controls {...{ group, fetchItems, setGroup }} />
{isLoading ? (
<Loader />
) : (
<List {...{ items, selected, selectItem, deselectItem }} />
)}
</div>
);
}
Here's the complete sandbox.
The state is managed in a reducer, because I need different parts of state to work and change together. For example, reset selected items on group change (because it makes no sense to keep selections between different data sets), set loaded items and clear loading state on data fetch success, etc. The example is intentionally simple, but in reality there're many dependencies between different parts of state (filtering, pagination, etc.), which makes reducer a perfect tool to manage it - in my opinion.
I've created helper functions to perform different actions (for ex., to reload items or to select/deselect). I could just pass down the dispatch to children and create action objects there, but this turns everything into a mess really quickly, esp. when multiple components must perform same actions.
Problem 1
Passing down reducer action functions to child components causes them to re-render on any reducer update.
Case 1: When I select an item in <List />, the <Controls /> is
re-rendered.
Case 2: When I reload the data on Reload button click, the <Controls /> is
re-rendered.
In both cases, the <Controls /> only actually depends on group prop to render, so when it stays the same - the component should not re-render.
I've investigated it and this happens because on each <App /> re-render these action functions are re-created and treated as new prop values for child components, so for React it's simple: new props => new render.
Not ideal solution to this is to wrap all action functions in useCallback, with dispatch as a dependency, but this looks like a hack to me.
const setGroup = useCallback(
(group) => {
dispatch({
type: SET_GROUP,
payload: { group }
});
},
[dispatch]
);
In a simple example it does not look too bad, but when you have dozens of possible actions, all wrapped in useCallback, with deps arrays - that does not seem right.
And it requires to add even more deps to useEffect (which is another problem).
Here's a "fixed" version with useCallback.
Problem 2
I cannot fully extract reducer action functions outside the <App /> component, because in the end they must be used inside a React component with the dispatch (because it's a hook).
I can of course extract them to a separate module and pass dispatch as a first argument:
in actions.js
// ...
export const fetchItems = (dispatch, group) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
// ...
and then in child components do this:
import { fetchItems } from './actions';
const Child = ({ dispatch, group }) => {
fetchItems(dispatch, group);
// ...
};
and reduce my <App /> to this:
// ...
const App = () => {
const [{ items, group, selected, isLoading }, dispatch] = useReducer(
itemsReducer,
itemReducerDefaults
);
useEffect(() => {
fetchItems(dispatch, group);
}, [group, dispatch]);
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading ? <Loader /> : <List {...{ items, selected, dispatch }} />}
</div>
);
};
but then I have to pass around the dispatch (minor issue) and always have it in arguments list. On the other hand, it fixes the Problem 1 as well, as dispatch does not change between renders.
Here's a sandbox with actions and reducer extracted.
But is it optimal, or maybe I should use some entirely different approach?
So, how do you guys use it? The React docs and guides are nice and clean with counter increments and ToDo lists, but how do you actually use it in real world apps?
React-redux works by also wrapping all the actions with a call to dispatch; this is abstracted away when using the connect HOC, but still required when using the useDispatch hook. Async actions typically have a function signature (...args) => dispatch => {} where the action creator instead returns a function that accepts the dispatch function provided by redux, but redux requires middleware to handle these. Since you are not actually using Redux you'd need to handle this yourself, likely using a combination of both patterns to achieve similar usage.
I suggest the following changes:
De-couple and isolate your action creators, they should be functions that return action objects (or asynchronous action functions).
Create a custom dispatch function that handles asynchronous actions.
Correctly log when a component renders (i.e. during the commit phase in an useEffect hook and not during any render phase in the component body. See this lifecycle diagram.
Pass the custom dispatch function to children, import actions in children... dispatch actions in children. How to avoid passing callbacks down.
Only conditionally render the Loader component. When you render one or the other of Loader and List the other is unmounted.
Actions (actions.js)
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import fetchItemsFromAPI from "./api";
export const setGroup = (group) => ({
type: SET_GROUP,
payload: { group }
});
export const selectItem = (id) => ({
type: SELECT_ITEM,
payload: { id }
});
export const deselectItem = (id) => ({
type: DESELECT_ITEM,
payload: { id }
});
export const fetchItems = (group) => (dispatch) => {
dispatch({ type: FETCH_START });
fetchItemsFromAPI(group).then((items) =>
dispatch({
type: FETCH_SUCCESS,
payload: { items }
})
);
};
useAsyncReducer.js
const asyncDispatch = (dispatch) => (action) =>
action instanceof Function ? action(dispatch) : dispatch(action);
export default (reducer, initialArg, init) => {
const [state, syncDispatch] = React.useReducer(reducer, initialArg, init);
const dispatch = React.useMemo(() => asyncDispatch(syncDispatch), []);
return [state, dispatch];
};
Why doesn't useMemo need a dependency on useReducer dispatch function?
useReducer
Note
React guarantees that dispatch function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
We want to also provide a stable dispatch function reference.
App.js
import React, { useEffect } from "react";
import useReducer from "./useAsyncReducer";
import Controls from "./Controls";
import List from "./List";
import Loader from "./Loader";
import { ItemGroups } from "./constants";
import {
FETCH_START,
FETCH_SUCCESS,
SET_GROUP,
SELECT_ITEM,
DESELECT_ITEM
} from "./constants";
import { fetchItems } from "./actions";
export default function App() {
const [state, dispatch] = useReducer(itemsReducer, {
items: [],
selected: [],
group: ItemGroups.PEOPLE,
isLoading: false
});
const { items, group, selected, isLoading } = state;
useEffect(() => {
console.log("use effect on group change");
dispatch(fetchItems(group));
}, [group]);
React.useEffect(() => {
console.log("<App /> render");
});
return (
<div className="App">
<Controls {...{ group, dispatch }} />
{isLoading && <Loader />}
<List {...{ items, selected, dispatch }} />
</div>
);
}
Controls.js
import React, { memo } from "react";
import { ItemGroups } from "./constants";
import { setGroup, fetchItems } from "./actions";
const Controls = ({ dispatch, group }) => {
React.useEffect(() => {
console.log("<Controls /> render");
});
return (
<div className="Controls">
<label>
Select group
<select
value={group}
onChange={(e) => dispatch(setGroup(e.target.value))}
>
<option value={ItemGroups.PEOPLE}>{ItemGroups.PEOPLE}</option>
<option value={ItemGroups.TREES}>{ItemGroups.TREES}</option>
</select>
</label>
<button onClick={() => dispatch(fetchItems(group))}>Reload data</button>
</div>
);
};
List.js
import React, { memo } from "react";
import { deselectItem, selectItem } from "./actions";
const List = ({ dispatch, items, selected }) => {
React.useEffect(() => {
console.log("<List /> render");
});
return (
<ul className="List">
{items.map(({ id, name }) => (
<li key={`item-${name.toLowerCase()}`}>
<label>
<input
type="checkbox"
checked={selected.includes(id)}
onChange={(e) =>
dispatch((e.target.checked ? selectItem : deselectItem)(id))
}
/>
{name}
</label>
</li>
))}
</ul>
);
};
Loader.js
const Loader = () => {
React.useEffect(() => {
console.log("<Loader /> render");
});
return <div>Loading data...</div>;
};
I'm trying to refactor a class into a stateless component using React hooks.
The component itself is very simple and I don't see where I'm making a mistake, as it's almost a copy paste from the react docs.
The component is showing a popup when the user clicks on a button (button is passed through props to my component). I'm using typescript.
I commented the line that fails to do what I want in the hooks version
Here's my original class:
export interface NodeMenuProps extends PropsNodeButton {
title?: string
content?: JSX.Element
button?: JSX.Element
}
export interface NodeMenuState {
visible: boolean
}
export class NodeMenu extends React.Component<NodeMenuProps, NodeMenuState> {
state = {
visible: false
}
hide = () => {
this.setState({
visible: false
})
}
handleVisibleChange = (visible: boolean) => {
this.setState({ visible })
}
render() {
return (
<div className={this.props.className}>
<div className={styles.requestNodeMenuIcon}>
<Popover
content={this.props.content}
title={this.props.title}
trigger="click"
placement="bottom"
visible={this.state.visible}
onVisibleChange={this.handleVisibleChange}
>
{this.props.button}
</Popover>
</div>
</div>
)
}
}
Here's the React hooks version:
export interface NodeMenuProps extends PropsNodeButton {
title?: string
content?: JSX.Element
button?: JSX.Element
}
export const NodeMenu: React.SFC<NodeMenuProps> = props => {
const [isVisible, setIsVisible] = useState(false)
const hide = () => {
setIsVisible(false)
}
const handleVisibleChange = (visible: boolean) => {
console.log(visible) // visible is `true` when user clicks. It works
setIsVisible(visible) // This does not set isVisible to `true`.
console.log(isVisible) // is always `false` despite `visible` being true.
}
return (
<div className={props.className}>
<div className={styles.requestNodeMenuIcon}>
<Popover
content={props.content}
title={props.title}
trigger="click"
placement="bottom"
visible={isVisible}
onVisibleChange={handleVisibleChange}
>
{props.button}
</Popover>
</div>
</div>
)
}
Much like setState, the state update behaviour using hooks will also require a re-render and update and hence the change will not be immedialtely visible. If however you try to log state outside of the handleVisibleChange method, you will see the update state
export const NodeMenu: React.SFC<NodeMenuProps> = props => {
const [isVisible, setIsVisible] = useState(false)
const hide = () => {
setIsVisible(false)
}
const handleVisibleChange = (visible: boolean) => {
console.log(visible) // visible is `true` when user clicks. It works
setIsVisible(visible) // This does not set isVisible to `true`.
}
console.log({ isVisible });
return (
<div className={props.className}>
<div className={styles.requestNodeMenuIcon}>
<Popover
content={props.content}
title={props.title}
trigger="click"
placement="bottom"
visible={isVisible}
onVisibleChange={handleVisibleChange}
>
{props.button}
</Popover>
</div>
</div>
)
}
Any action that you need to take on the basis of whether the state was update can be done using the useEffect hook like
useEffect(() => {
// take action when isVisible Changed
}, [isVisible])
i wanted to create multiple request from hoc,where i was able to create hoc for single request(redux action which call api),please check the code for single request
i have created hoc for reducing repeated code in every component,like on componentdidmount calling api,managing error,managing loading state but it is only for single request like you can see in intial object in given hoc,so i want a to create hoc which can executes for multiple request(redux action which calls api),i dont know that this solution which is working for single request is properly implemented or not
So,please help me to create hoc which can be resealable for any given scenario
hoc
export const ComponentWithAPIRequest = ({ onMount = null, LoaderRequestID,onUnmount = null }) => (WrappedComponent) => {
return class ComponentWithAPIRequest extends Component {
state = {
stateLoader: true // intial Load
};
componentDidMount = () => {
this.Request();
};
componentWillUnmount() {
onUnmount !== null ? this.props[onUnmount]() : null;
}
Request = () => {
onMount !== null ? this.props[onMount](LoaderRequestID) : null; // API request
this.setState({ stateLoader: false });
};
render() {
const { error, isLoading } = this.props; // pass it here somehow.
const { stateLoader } = this.state;
const isLoadingFromAPI = this.props.isLoadingRequestIds.indexOf(LoaderRequestID) !== -1 ? true : false;
if (stateLoader) {
return (
<div className="text-center">
<CircularProgress />
</div>
);
}
if (isLoadingFromAPI) {
return (
<div className="text-center">
<CircularProgress />
</div>
);
} else {
return <WrappedComponent {...this.props} retry={this.Request} />;
}
}
};
};
component
export const isContainer = ({ intial, list }) => (WrappedComponent) => {
const IsContainer = (props) => <WrappedComponent {...props} />;
return compose(ComponentWithAPIRequest(intial), hasRequestError)
(IsContainer);
};
hasRequestError // is error hoc
ComponentWithAPIRequest // is above added hoc
#isContainer({
intial: {
onMount: 'FirstRequest', // is a redux action which call api,here i want to have multiple request like ['FirstRequest','SecondRequest']
LoaderRequestID: 'FirstRequestLoader', // is an unique id for loader,which then for multiple request be converted to respective request like ['FirstRequestLoader','SecondRequestLoader']
onUnmount: 'ResetLeaderBoardAll' // is a redux action when component unmount
}
})
class ComponentView extends Component {
render() {
return (
<SomeComponent {...this.props}/>
);
}
}
const mapStateToProps = (state) => {
return {
somestate:state
};
};
export default connect(mapStateToProps, {
FirstRequest,
SecondRequest
})(ComponentView);
If I were you, I would update your hoc and pass multiple URLs as parameter to send multiple requests like:
export default connect(mapStateToProps, mapDispatchToProps)(ComponentWithAPIRequest(ComponentView,
[
"url1", "url2" ...
]));
And pass them back to ComponentView with 'props' or 'props.requestResponses'(single) object. Then I would use them like
const url1response = this.props.requestResponses["url1"];
In my case, I would store them in ComponentWithAPIRequest's state but you can use redux and get them from mapStateToProps in ComponentView as well.
I have a problem with React.
When I press the "+" button, this console message appears and nothing happens:
Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`
I found several questions with similar titles, but common thing among them is that there were calls of functions with setState inside render method.
My render method has no calls, but error appears.
Why?
Thank you for reading.
Code:
import React from 'react';
const TodoForm = ({addTodo}) => {
let input;
return (
<div>
<input
ref={node => {
input = node;
}}
/>
<button onClick={() => {
addTodo(input.value);
input.value = '';
}}>
+
</button>
</div>
);
};
const Todo = ({todo, remove}) => {
// Each Todo
return (<li onClick={remove(todo.id)}>{todo.text}</li>)
};
const TodoList = ({todos, remove}) => {
// Map through the todos
const todoNode = todos.map((todo) => {
return (<Todo todo={todo} key={todo.id} remove={remove}/>)
});
return (<ul>{todoNode}</ul>);
};
const Title = () => {
return (
<div>
<div>
<h1>to-do</h1>
</div>
</div>
);
};
window.id = 0;
class TodoApp extends React.Component {
constructor(props) {
// Pass props to parent class
super(props);
// Set initial state
this.state = {
data: []
}
}
// Add todo handler
addTodo(val) {
// Assemble data
const todo = {text: val, id: window.id++}
// Update data
this.state.data.push(todo);
// Update state
console.log('setting state...');
this.setState({data: this.state.data});
}
// Handle remove
handleRemove(id) {
// Filter all todos except the one to be removed
const remainder = this.state.data.filter((todo) => {
if (todo.id !== id) return todo;
});
// Update state with filter
this.setState({data: remainder});
}
render() {
// Render JSX
return (
<div>
<Title />
<TodoForm addTodo={
(val)=>{
this.addTodo(val)
}
}/>
<TodoList
todos={this.state.data}
remove={this.handleRemove.bind(this)}
/>
</div>
);
}
}
export default TodoApp;
In your render method for Todo you invoke remove, which is where your erroneous state update happens.
To fix this, return a function from the handleRemove method of TodoApp that updates the state. Simplified version:
handleRemove(id) {
return () => {
...
this.setState({ data: remainder });
}
}
Also worth noting here that because you're using the current state, it's best to use the setState callback (which gets prevState as an argument), and not rely on this.state.
setState docs
Andy_D very helped and my answer has two solutions:
First in render function change
<TodoList
todos={this.state.data}
remove={this.handleRemove.bind(this)}
/>
to
<TodoList
todos={this.state.data}
remove={() => this.handleRemove.bind(this)}
/>
or change code
const Todo = ({todo, remove}) => {
// Each Todo
return (<li onClick={remove(todo.id)}>{todo.text}</li>)
};
to that:
const Todo = ({todo, remove}) => {
// Each Todo
return (<li onClick={() => remove(todo.id)}>{todo.text}</li>)
};
i have some issues dealing with a simple case in my redux-react app: i want to reset an input text after an asynchronous operation ignited by a button.
Let’s say we have an input text in which you put a text and this is passed through a onClick event to a dispatch action.
This action contacts a server and after the server response i want to reset the input field.
I’ve implemented a number of solutions (i’m using redux thunk) to this problem but i’m not sure if they are hacky ways to solve it, let me show you:
1) Presentational component (the input field) implements a reset method that is passed as a value to the onClick method.
export default React.createClass({
reset: function () {
this.setState({searchText: ''})
},
getInitialState: function () {
return {
searchText: ''
}
},
render: function () {
return (
<div>
<TextField
value={this.state.searchText}
onChange={e => this.setState({ searchText: e.target.value })}
/>
<RaisedButton
onClick={this.props.startSearch.bind(null,
this.state.searchText,
this.reset)} // ===> HERE THE RESET FUNCTION IS PASSED
/>
</div>
)
}
})
The container dispatches the action and then calls the reset method.
const mapDispatchToProps = (dispatch) => {
return {
startSearch: (searchText, reset) => {
dispatch(actions.startSearch(searchText))
.then(() => reset())
}
}
}
2) Using ref (https://facebook.github.io/react/docs/refs-and-the-dom.html)
The container gets a reference to its child and calls reset through it
const SearchUserContainer = React.createClass({
startSearch: (searchText) => {
dispatch(actions.startSearch(searchText))
.then(() => this.child.reset())
},
render: function () {
return (
<SearchUser {...this.props} ref={(child) => { this.child = child; }}/>
)
}
})
3) The Redux Way.
searchText is managed by the store thus the action dispatched triggers a resolver that reset the searchText value, the container updates its child and we are done, well… almost:
the presentational component is a controlled component (https://facebook.github.io/react/docs/forms.html#controlled-components) that means it manages the input text as an internal state, i think we have to find a way to make the two ‘state managers’ coexist.
I wrote this code to manage the internal state and the state coming from redux, in few words the presentational gets the initial value from redux, then updates it in the onChange event and it’s ready to receive updates from redux thanks to componentWillReceiveProps.
export default React.createClass({
getInitialState: function () {
return {
searchText: this.props.searchText ==> REDUX
}
},
componentWillReceiveProps: function (nextProps) {
this.setState({
searchText: nextProps.searchText ==> REDUX
})
},
render: function () {
return (
<div>
<TextField
value={this.state.searchText}
onChange={e => this.setState({ searchText: e.target.value })}
/>
<RaisedButton
onClick={this.props.startSearch.bind(null, this.state.searchText)}
/>
</div>
)
}
})
4) Redux-Form
To complete the picture i link the redux-form options to do that
http://redux-form.com/6.5.0/docs/faq/HowToClear.md/
What do you think about those ideas?
Thanks.
Go the Redux way, except go all the way: remove the internal state from your component completely and let Redux handle it (might as well make your component a pure-functional component too):
Component:
import { connect } from 'redux';
import { actions } from 'actionCreators';
const ControlledInputComponent = (props) => {
return (
<div>
<TextField
value={this.props.searchText}
onChange={e => this.props.setSearchText(e.target.value)}
/>
<RaisedButton
onClick={this.props.startSearch}
/>
</div>
);
};
const mapStateToProps = (state) => {
return { searchText: state.searchText };
};
const mapDispatchToProps = (dispatch) => {
return {
setSearchText: (txt) => { dispatch(actions.setSearchText(txt)); },
startSearch: () => { dispatch(actions.search()); }
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ControlledInputComponent);
Action creator:
export const actions = {
setSearchText: (txt) => ({ type: 'setText', data: txt }),
//here's where the thunk comes in
//make sure you have redux-thunk and add it as a middleware when setting up the store, etc.
search: () => {
return (dispatch) => {
//use fetch or whatever to run your search (this is a simplified example)
fetch(/* your url here */).then(() => {
//presumably a success condition
//handle the search results appropriately...
//dispatch again to reset the search text
dispatch(actions.setSearchText(null);
});
};
}
};
Reducer:
const reducer = (state = { searchText: null }, action) => {
if (!action || !action.type) return state;
switch (action.type) {
//you should really define 'setText' as a constant somewhere
//so you can import it and not have to worry about typos later
case 'setText':
return Object.assign({}, state, { searchText: action.data });
default:
return state;
}
};
export default reducer;
Hopefully that helps. Good luck!