I am using redux with connect and redux-thunk middleware and containers.
Currently when a user perform an action, example one click on a button, I need to dispatch that action (sync) which will dispatch other few actions (asynch).
I am aware dispatching actions from within the reducer is an anti pattern.
I would like to know what is a suitable place for this code.
Currently I am not sure if it should stay in:
The action creator.
In the container using store.subscribe.
The recommended way as per the documentation is in the action creator, like so:
function actionCreator(payload) {
return dispatch => {
dispatch(action1(payload))
dispatch(action2(payload))
}
}
Then you would probably want to attach the action creators as prop and pass it down to the container using mapDispatchToProps like in the example mentioned here. So it would look something like so:
const mapDispatchToProps = dispatch => ({
action1: some_payload => dispatch(action1(some_payload))
action2: some_payload => dispatch(action2(some_payload))
})
// your component
export default connect(mapStateToProps, mapDispatchToProps)(YourApp)
As other pointed out The action creator is the right place for dispatching multiple actions.
Below an example of how action1 could dispatch other actions in your action creator.
const action1 = id => {
return dispatch => {
dispatch(action2(id))
dispatch(action3(id))
}
}
The action creator is the correct location for dispatching multiple actions. Although code like the following would work:
function actionCreator(payload) {
return dispatch => {
dispatch(action1(payload))
dispatch(action2(payload))
}
}
I would highly recommend redux-thunk based action creators to always return a resolved Promise, so that such action creators can be part of another async call. So, the simplest update to the above would be:
function actionCreator(payload) {
return dispatch => {
dispatch(action1(payload));
dispatch(action2(payload));
return Promise.resolve();
}
}
It would then be possible to dispatch to the above with:
actionCreator(payload).then(doAnotherAction(anotherPayload))
or the following, if we need to maintain order of calls:
actionCreator(payload).then(() => doAnotherAction(anotherPayload))
If you wish to 'future-proof' your action creator, so that it could handle calling both async and sync action creators, you could write it as:
function actionCreator(payload) {
return dispatch =>
Promise.resolve(dispatch(action1(payload))).then(
() => dispatch(action2(payload)));
}
And, if you like ES6 arrow notation the above can be defined as:
const actionCreator = payload => dispatch =>
Promise.resolve(dispatch(action1(payload))).then(
() => dispatch(action2(payload)));
If you have a Promise Middleware, you can use this syntax so you're able to use .then() on your dispatch(topLevelAction()):
export const topLevelAction = () => dispatch => {
return Promise.all([dispatch(action1()), dispatch(action2()), dispatch(action3())])
}
While solution by #GibboK did not work for me:
const mapDispatchToProps = (dispatch) => ({
action2: id => dispatch(Actions.action2(id)),
action3: id => dispatch(Actions.action3(id)),
action1: (dateId, attrId) => {
return dispatch => {
dispatch(Actions.action2(dateId));
dispatch(Actions.action3(attrId));
}
}
});
I eventually went with redux-batched-actions. Worked like charm:
const mapDispatchToProps = (dispatch) => ({
action2: id => dispatch(Actions.action2(id)),
action3: id => dispatch(Actions.action3(id)),
action1: (dateId, attrId) =>
dispatch(batchActions([
Actions.action2(dateId),
Actions.action3(attrId)
]))
});
"had similar issue. had to create a function
that accepts object with the actions you want to
dispatch to the store and individual params for
respective action"
dispatchMultiple({params: {
params1: "<arg for first action>" ,
params2: "<arg for second action>",
},
})
const dispatchMultiple = (obj) => {
dispatch(obj.actions.action1(obj.params.params1));
dispatch(obj.actions.action2(obj.params.params2));
};
This is what worked for me for synchronous multiple actions:
// utils file
const multipleActions = (dispatch) => {
dispatch(action1())
dispatch(action2())
dispatch(action3())
}
const mapDispatchToProps = (dispatch) => {
onClickReturn: () => {
multipleActions(dispatch)
}
};
For guys in 2020...
The actions are Supposed to be made in the action Creater. For those who would like to dispatch an action and fetch/post some data from the API can use this Idea.
lets assume we have an actions.js file and we want to dispatch a loading action before fetch data.
function requestPosts() {
return {
type: "loading"
}
}
This is the fetching action function
function fetchPosts() {
return dispatch => {
// dispatch the loading
dispatch(requestPosts());
// fetch data from api
return fetch("https://www.yoururl.com/api")
.then(response => response.json())
.then(json => dispatch({
type: "fetching successful",
payload: json
}));
}
}
I don't know the exact use case but since redux uses asynchronous logic, any solution that runs the second dispatch in the next tick of the event loop should work.
store.dispatch({ type: 'ADD_TODO', text: 'Buy milk.' });
setTimeout(() => {
store.dispatch({ type: 'ADD_TODO', text: 'Take out garbage.' });
}, 0);
Promise.resolve(() => {
store.dispatch({ type: 'ADD_TODO', text: 'Water plants.' });
});
If the second dispatch depends on the actions of first dispatch, you can get the state from the store, check if it satisfies the condition then dispatch the second action. It is best to keep the action's logic clean and granular.
So, to answer the question, the right place to dispatch multiple actions is inside the click handler where the first action originates.
The easiest way is to use a specialized middleware redux-soldier:
import { createStore, applyMiddleware } from 'redux'
import { reduxSoldierMiddleware } from 'redux-soldier'
const store = createStore(rootReducer, applyMiddleware(reduxSoldierMiddleware))
store.dispatch([
{type: 'INCREMENT'}, // traditional action
addTodo('Start using redux-soldier'), // action creator
fetchUser(), // thunk action
])
redux-soldier is also a full replacement for redux-thunk
For more info check the documentation redux-soldier.
Related
I'm creating a react app with redux.
I need the lists of french departements for all pages in my app, so I put it in redux state.
I dispatch the action in the App component in the useEffect hook (Note I use an other useEffect in the component, but when the action is in the other block it's not working too)
I have a page where I need to use this list, so I select it with the useSelector hook.
But it returns an empty object, I have an error telling me dpts.map is not a function
I think the action is dispatching after the page has rendered, because I when I log the response of the api call in the action, it appears after the log of the useSelector result.
I'm using another state property in another page, but it seems to work with the other page.
App.jsx
const dispatch = useDispatch();
useEffect(() => {
dispatch(getDpts());
}, [dispatch])
Here is the action associated with :
dpts.actions.js
import axios from "axios";
export const GET_DPTS = "GET_DPTS";
export const getDpts = () => {
return async (dispatch) => {
try {
const res = await axios({
method: "get",
url: "https://geo.api.gouv.fr/departements",
});
console.log("done : " + res)
dispatch({ type: GET_DPTS, payload: res.data });
} catch (err) {
(err) => console.log("DPTS FETCH ERROR --- " + err);
}
};
};
Map.jsx
function DptCtl() {
// Control
const map = useMap();
// List of dpts and provinces
const dpts= useSelector(dptsSelector);
console.log(dpts);
return (
<>
<input type="text" list="dpt-ctl-list" placeholder="Filtrer par département"/>
<datalist id="dpt-ctl-list">
{dpts.map((dpt, index) =>
<option value={dpt.code} key={index}>{dpt.nom}</option>
)}
</datalist>
</>
)
}
It depends on how you are initializing your state in the reducer.
for example you create a reducer with this initial state:
const initialState={}
later, based on actions, the state changes to this:
{dpts:someDataArray}
the problem is that you have a dpts.map somewhere in your app, since dpts is undefined in the beginning you receive that error that dpts.map is not a function.
the solution to this is simply put dpts in the initialState as an empty array:
const initialState={dpts:[]}
if that is not the issue with your code, meaning dpts isn't undefined in the initialState, it is probably initialized as a string or an object which don't have map methods.
I've got a weird problem where I dispatch an action correctly, but the reducer doesn't get called. I'm using redux-thunk and everything is set up exactly the same how it's set up in one of my other projects. The action type matches, but nothing happens.
Also, if I put a console.log on the default case of the reducer, it gets logged 4 times every time I refresh the page, so I suppose the reducer gets loaded correctly.
export const submitForm = names => {
console.log(names); //This logs what it's supposed to log every time
// I press the submit button
return dispatch => {
dispatch({
type: "SUBMIT_FORM",
names
});
};
};
export default function form(state = {}, action) {
switch (action.type) {
case "SUBMIT_FORM":
console.log(action.names);
return Object.assign({}, state, {
names: action.names
});
default:
return state;
}
}
//My component is connected like this:
const mapStateToProps = state => ({});
const mapDispatchToProps = dispatch => ({
formActions: bindActionCreators(formActions, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(Home);
It seems a little strange to me that an action creator returns a function. Action creatorsnormally return an action, that is, an object. Async action creators returns functions. So, you can try refactor your action creator that will return a objetct, and then you pass the action creator to the dispatch as an argument.
export const submitForm = names => {
console.log(names); //This logs what it's supposed to log every time
// I press the submit button
return {
type: "SUBMIT_FORM",
names
}
};
dispatch(submitForm(names));
Besides that, according to react-redux.js.org/mapdispatch to use bindActionCreators in mapDispatchToProps, mapDispatchToProps does not return an object, but does return bindActionCreators.
const mapDispatchToProps = dispatch =>
bindActionCreators(formActions, dispatch);
another point, if you need to pass an argument to your action creator I would do like this:
const mapDispatchToProps = dispatch => ({
submitForm: (names) => dispatch(submitForm(names))
});
Unfortunately, I can't see all of your code. I hope I helped you with something.
I've just started working with React, and wrote a couple of HOCs. Then I read that Functional Components and hooks are the suggested way of moving forward, so I decided to rewrite my HOCs.
A common pattern I used was:
class MyButton(Component) {
...
handleClick = () => {
setState({busy: True}, () => {
fetchAPI().then(
(results) => {
setState({results: results, errs: [], busy: False});
},
(err) => {
setState({errs: err, busy: False});
});
});
};
render() {
return(
<Button disabled={this.state.busy} onClick={this.handleClick} />
)
}
}
If I understand correctly, when I click the button, it would disable it, render the disabled button, then fetch things from the api, then set the state with the results (and enable the button), then render the enabled button.
This would prevent people from clicking the button 10 times, hitting the api 10 times before the first is finished.
How do I do this with functional components and hooks?
My naive implementation would be this, but it seems incorrect.
MyButton = (props) => {
const [results, setResults] = useState([]);
const [errs, setErrs] = useState([]);
const [busy, setBusy] = useState(False);
const handleClick = () => {
setBusy(true);
fetchAPI().then(
(results) => {
setResults(results);
setErrs([]);
setBusy(False);
},
(err) => {
setErrs(errs);
setBusy(False);
});
);
}
return (
<Button disabled={busy} onClick={handleClick} />
)
}
Is there a better way?
I don't see anything wrong with your implementation. It should work. But if you are looking for a better way, you can consider using a reducer and the useReducer hook.
Define a reducer for all your states, including error, loading and loaded states. In loading state set disabled to be false. In loaded state you would have disabled true. In your fetchApi function, you can dispatch the various actions. Before API call, dispatch a loading state and after the results are back, a loaded state with the payload containing your results. You can then use the reducer in your component with a useReducer hook which will get you the state and the dispatch. Use the state.disabled to disable the button.
Helpful SO discussion around useReducer
Good article on how to use useReducer for API calls
One naive way is to write your fetchApi in a useEffect hook and your button toggles when to activate this effect like the following. You could also check out react-query library to do this
useEffect(()=>{
const loadApi = () => {
setBusy(true);
fetchAPI().then(
(results) => {
setResults(results);
setErrs([]);
setBusy(False);
},
(err) => {
setErrs(errs);
setBusy(False);
});
);
}
if (toggle) {
loadApi();
setToggle(false)
}
},[toggle])
const handleOnClick = () => {
setToggle(true)
}
Supposing I have an update comment action. When a user updates comment after getting a successful result from Promise I should close comment editor. This is my sample code from my project:
export const updateComment = (comment,callBack/* ? */) => {
return (dispatch, getState){
api.updateComment({...comment}).then((result) => {
/* Do something */
callback() /* ? */
})
}
}
In react component I use action like the following code:
handleUpdateComment(){
dispatch(actions.updateComment(this.state.comment,this.closeCommentEitor)
}
It works well but I think is not a good pattern to close comment editor. I'm looking a correct pattern to close editor without passing callBack like I did if any.
When you are using redux-thunk, you can dispatch an action from another action.
What you can do is that, commentEditor have a state which you store in redux and based on that state open or close the commentEditor
export const updateComment = (comment, comment_id) => {
return (dispatch, getState){
api.updateComment({...comment}).then((result) => {
/* Do something */
dispatch({type: 'CLOSE_COMMENT_EDITOR', id: comment_id})
})
}
}
After this in a reducer on this action change the state of redux store, something like
import update from 'immutability-helper'
var initialState = [{commentId: '1', commentEditorOpenStatus: false}, {commentId: '2', commentEditorOpenStatus: false}]
const reducer = (state = initialState, action) => {
switch(action.type) {
'CLOSE_COMMENT_EDITOR':
const idx = state.findIndex(obj => obj.commentId == action.id);
return update(state, {
[idx]: {
commentEditorOpenStatus: {
$set: false
}
}
})
// Other action handlers here
default: return state
}
}
The only thing that updates your application's state is your reducers.
The reducer should be responsible to update the state of your application and not your action (you are now passing getState).
I suggest you to look at redux-promise-middleware
The middleware enables optimistic updates and dispatches pending, fulfilled and rejected actions, which can be intercepted by the reducer.
The code in my reducer is as follows:
import {ADD_FILTER, REMOVE_FILTER} from "../../../../actions/types";
const removeFilter = (state, name) => {
return state.filter(f => f.name !== name);
};
export default function addRemoveFilterReducer(state = [], action) {
switch (action.type) {
case ADD_FILTER:
let nState = state;
if(state.some(i => i.name === action.searchFilter.name))
nState = removeFilter(state, action.searchFilter.name);
return [...nState, {name: action.searchFilter.name, values: [action.searchFilter.values]}];
//Call another action
case REMOVE_FILTER:
return removeFilter(state, action.searchFilter.name);
default:
return state;
}
}
I have one component showroom and inside the showroom I have Search component and Content component.
Inside search component I handle filtering and I dispatch an action which is handled with this reducer.
After the filter is added I want to dispatch an action with all filters. How can I do that?
That action would be handled with an reducer where I would return only those cars that match search criteria and display them in the content component.
I hope you understand what I wanted to say.
Is this approach good at all?
You may consider to use redux-thunk for this.
You'll have two separate actions, one for adding filter and the other one for making search. Your addFilterAndMakeSearch thunk will be responsible for calling these actions in order. By this way, you won't be need to dispatch an action from your reducer.
// Thunk
function addFilterAndMakeSearch(searchFilter) {
return dispatch => {
dispatch(addFilter(searchFilter);
dispatch(makeSearch());
}
}
// Action Creator One
function addFilter(searchFilter) {
return {
type: 'ADD_FILTER',
payload: searchFilter,
};
}
// Action Creator Two
function makeSearch() {
return {
type: 'MAKE_SEARCH',
};
}
In order to make this work, you need to use addFilterAndMakeSearch as your onSubmit handler.
Calling an action is most probably side effect operation. As reducers should follow functional programming principles, I would argue it shouldn't trigger any actions.
To solve your use case, you can still fire two actions from place that triggered change in your reducer. We are doing that sometimes in our app. For example your component can trigger two actions or action can fire two Redux store updates.