Good pattern for redux action in callback - javascript

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.

Related

React hook, wired issue when use useState, while if use setState work perfectly, how to solve it

dear community, I am facing a wired issue, and I don't know how to summary my situation in the question title, so I wonder if the question title is accurate enough.
I was trying to convert a class component to a hook component.
The class version code like this
async componentDidMount() {
const { dispatch, itemId } = this.props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment', //here to fetch submissions in props
payload: {
id: itemId
}
});
}
const { submissions } = this.props;
this.setState({
studentSubmissions: submissions,
});
} catch (error) {
throw error.message;
}
}
render() {
const { studentSubmissions } = this.state;
return (
<Table dataSource={studentSubmissions} />
)
}
export default SubmissionsDetail;
and in hook, it look like this
const [studentSubmissions, setStudentSubmissions] = useState([]);
useEffect(() => {
async function fetchSubmissions() {
const { dispatch, itemId } = props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment',
payload: {
id: itemId
}
});
}
const { submissions } = props;
setStudentSubmissions(submissions)
} catch (error) {
throw error.message;
}
};
fetchSubmissions()
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
I omitted some code for better reading, like connect to redux store or others.
and the component is import in the parent file like this
import SubmissionsDetail from './SubmissionsDetail'
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId}/>
})}
it work perfect in class component, the expected result should return tables like this
However, when I change to use hook, the result return like this
or sometimes all data in tables become submissions3
I try to console.log(submissions) inside the try{...} block, when in class, the result is
which is correct, there have two assignments, the one have 4 submissions, another one have zero submission.
But the output in hook is different, the result is like this
either both have 4 submissions, either both have zero. That means one obj affect all other obj.
It seems like if useState change, it would influence other objs, that make me really confused. I think in the map method, each item is independent, right? If so, and how to explain why it work perfectly in class setState, but failed in hook useState?
I hope my question is clear enough, If you know how to describe my question in short, plz let me know, I would update the title, to help locate experts to answer.
Please don't hesitate to share your opinions, I really appreciate and need your help, many thanks!
Edit: You are probably going to want to rework the way you store the submission inside of the redux store if you really want to use the Hook Component. It seems like right now, submissions is just an array that gets overwritten whenever a new API call is made, and for some reason, the Class Component doesn't update (and it's suppose to update).
Sorry it's hard to make suggestions, your setup looks very different than the Redux environments I used. But here's how I would store the submissions:
// no submissions loaded
submissions: {}
// loading new submission into a state
state: {
...state,
sessions: {
...state.session,
[itemId]: data
}
}
// Setting the state inside the component
setStudentSubmissions(props.submissions[itemId])
And I think you will want to change
yield put({
type: 'getSubmissions',
payload: response.data.collections
});
to something like
yield put({
type: 'getSubmissions',
payload: {
data: response.data.collections,
itemId: id
});
If you want to try a "hack" you can maybe get a useMemo to avoid updating? But again, you're doing something React is not suppose to do and this might not work:
// remove the useEffect and useState, and import useMemo
const studentSubmissions = useMemo(async () => {
try {
if (itemId) {
await dispatch({
type: "assignment/fetchSubmissionsByAssignment", //here to fetch submissions in props
payload: {
id: itemId,
},
});
return this.props.submissions;
}
return this.props.submissions;
} catch (error) {
throw error.message;
}
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
There is no reason to use a local component state in either the class or the function component versions. All that the local state is doing is copying the value of this.props.submissions which came from Redux. There's a whole section in the React docs about why copying props to state is bad. To summarize, it's bad because you get stale, outdated values.
Ironically, those stale values were allowing it to "work" before by covering up problems in your reducer. Your reducer is resetting the value of state.submissions every time you change the itemId, but your components are holding on to an old value (which I suspect is actually the value for the previous component? componentDidMount will not reflect a change in props).
You want your components to select a current value from Redux based on their itemId, so your reducer needs to store the submissions for every itemId separately. #Michael Hoobler's answer is correct in how to do this.
There's no problem if you want to keep using redux-saga and keep using connect but I wanted to give you a complete code so I am doing it my way which is with redux-toolkit, thunks, and react-redux hooks. The component code becomes very simple.
Component:
import React, { useEffect } from "react";
import { fetchSubmissionsByAssignment } from "../store/slice";
import { useSelector, useDispatch } from "../store";
const SubmissionsDetail = ({ itemId }) => {
const dispatch = useDispatch();
const submissions = useSelector(
(state) => state.assignment.submissionsByItem[itemId]
);
useEffect(() => {
dispatch(fetchSubmissionsByAssignment(itemId));
}, [dispatch, itemId]);
return submissions === undefined ? (
<div>Loading</div>
) : (
<div>
<div>Assignment {itemId}</div>
<div>Submissions {submissions.length}</div>
</div>
);
};
export default SubmissionsDetail;
Actions / Reducer:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchSubmissionsByAssignment = createAsyncThunk(
"assignment/fetchSubmissionsByAssignment",
async (id) => {
const response = await getSubmissionsByAssignment(id);
// can you handle this in getSubmissionsByAssignment instead?
if (response.status !== 200) {
throw new Error("invalid response");
}
return {
itemId: id,
submissions: response.data.collections
};
}
);
const initialState = {
submissionsByItem: {}
};
export default createReducer(initialState, (builder) =>
builder.addCase(fetchSubmissionsByAssignment.fulfilled, (state, action) => {
const { itemId, submissions } = action.payload;
state.submissionsByItem[itemId] = submissions;
})
// could also respond to pending and rejected actions
);
if you have an object as state, and want to merge a key to the previous state - do it like this
const [myState, setMyState] = useState({key1: 'a', key2: 'b'});
setMyState(prev => {...prev, key2: 'c'});
the setter of the state hook accepts a callback that must return new state, and this callback recieves the previous state as a parameter.
Since you did not include large part of the codes, and I assume everything works in class component (including your actions and reducers). I'm just making a guess that it may be due to the omission of key.
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId} key={itemId} />
})}
OR it can be due to the other parts of our codes which were omitted.

Redux reducer - on state change

I want to save the current state of a specific reducer into session storage without explicitly calling it on every action.
const { handleActions } = require("redux-actions");
// How do i make this logic run on any state change without adding it to every action?
const onStateChange = (state) => sessionStorage.save(keys.myReducer, state);
const myReducer = handleActions
({
[Actions.action1]: (state, action) => (
update(state, {
prop1: {$set: action.payload},
})
),
[Actions.action2]: (state, action) => (
update(state, {
prop2: {$set: action.payload},
})
)
},
//****************INITIAL STATE***********************
{
prop1: [],
prop2: []
}
);
Is there a way to catch the state change event for a specific reducer?
I know of store.subscribe, but that's for the entire store, i'm talking about listening to a specific reducer change
Thanks
Unfortunately you can't watch specific parts of your store because your store is actually an only reducer (combineReducers return a single big reducer). That said you can manually check if your reducer has changed by doing state part comparison.
let previousMyReducer = store.getState().myReducer;
store.subscribe(() => {
const { myReducer } = store.getState();
if (previousMyReducer !== myReducer) {
// store myReducer to sessionStorage here
previousMyReducer = myReducer;
}
})
You can use redux-saga to listen for specific actions and persist the store (and do your other logice) when they are fired. This seems like a good fit for your needs.
It's not exactly tied to specific store changes, but rather to a set of actions. However, I think this model (listening for actions, as opposed to state changes) is better suited to the redux design.
The nice thing is you don't have to change any code outside of the saga. I've used this one for autosaving form data and it worked great.
Here's an example of setting up a saga to persist on a few redux-form actions; note that it's not limited to persisting - you can do whatever you want during a saga.
function* autosaveSaga(apiClient) {
yield throttle(500,
reduxFormActionTypes.CHANGE,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_PUSH,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_POP,
saveFormValues,
apiClient);
}
If you want to listen to all action types, or do custom filtering of which actions fire your persistence code, you can pass a pattern, or a matching function to takeLatest:
yield takeLatest(
'*',
saveFormValues,
apiClient);
More on what you can pass to take, takeLatest etc can be found in the docs for take.

Where to dispatch multiple actions in redux?

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.

Call an action inside the redux 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.

Redux: dispatching an action according to cross-reducers state

This is my combined reducers:
export const element = (state, action) => {
...
}
export const elementSelection = (state, action) => {
...
}
const holoApp = combineReducers({
elements,
elementSelection
})
The state of elementSelection holds the current selected element. I want to be able to dispatch an action in the elementsReducer according to the current selected element.
For instance if store.getState().elementSelection.elementType equals "placeholder" this action will be disptached:
store.dispatch(replaceElement(...));
And otherwise this action will be dispatched:
store.dispatch(addElement(...));
My only guess is to put this logic in the app class which consists of the store and I wonder whether it is the best practice for dispatching actions according to cross-reducers state.
you can achive that in action creator with redux-thunk
const someAction = elem => (dispatch, getState)=>{
if(getState().elementSelection.elementType == "placeholder" ){
dispatch(replaceElement(...));
}else{
dispatch(addElement(...));
}
}

Categories

Resources