I have the intention to perform a fetch to my rest api when ever the user updates an input bar. The problem I am having is that the componentDidUpdate method calls the action creator which dispatches the json to the reducer, in turn updating the state and calling componentDidUpdate again. Any ideas or best practices to end the endless cycle? Thanks!
Action Creator:
export const fetchFoods = (dispatch, searchText) => {
return fetch(`/nutrition/${searchText}`)
.then(res => res.json())
.then(json => dispatch({type: 'RECEIVE_FOODS', json}))
}
Reducer:
const foods = (state = [], action) => {
switch (action.type) {
case 'RECEIVE_FOODS':
return action.json
default:
return state
}
}
React Container:
const FoodList = React.createClass({
componentDidUpdate: function() {
fetchFoods(this.props.dispatch, this.props.searchText)
},
componentWillMount: function() {
fetchFoods(this.props.dispatch, this.props.searchText)
},
render: function() {
...
}
})
export default connect(
(state) => {
return {searchText: state.searchText, foods: state.foods}
}
)(FoodList)
You should remove the fetchFoods function from the componentDidUpdate. Calling the function on an update will result in an infinite loop like you describe.
Once the component has mounted the first time and retrieved the original set of data, you should only call the action when it's explicitly required. If the user changes it, another function changes it, etc.
Attach an onChange() handler to your input (or whatever you want to trigger an update), so that whenever input changes, it calls a function that explicitly dispatches that action.
https://jsfiddle.net/julianljk/69z2wepo/49105/
Function:
handleChange: function (e) {
var myInput = e.target.value;
this.setState({
value: myInput
});
fetchFoods(this.props.dispatch, myInput) //sends it to the store,
},
Render:
render: function (){
return(
<div>
<input type="text" value={this.state.value} onChange={this.handleChange}/>
</div>
)}
Putting action dispatches in lifecycle methods can generally cause this sort of behavior.
Related
I have this simple React custom hook with a data list and remove/undo funcionality.
Now, I need to call a delete request on cleanup. The problem is the cleanup is called as an async onClose callback with a timeout delay, so when I delete an item and go to another page before the timeout runs out, the dispatch/reducer is not being called. I need to call the API delete request with the updated state.toRemove array which is only accessible in the reducer, even after the component unmounts.
The logs in the example code shows that after unmount, the cleanup function is being called, however the reducer with the updated state is not.
This example is based on React Toastify undo funcionality (which has no delete API calls)
Is this just a React limitation or is there some solution to it? Thank you!
import React, { useState, useEffect, useReducer } from 'react'
import { toast } from 'react-toastify'
function reducer(state, action) {
console.log("I'm not being called after component unmount.")
switch (action.type) {
case "LOAD_COLLECTION":
return {
collection: action.collection,
toRemove: state.toRemove
};
case "QUEUE_FOR_REMOVAL":
return {
collection: state.collection,
toRemove: [...state.toRemove, action.id]
};
case "CLEAN_COLLECTION": {
console.log("I'm not being called neither. API delete would go here.")
return {
collection: state.collection.filter(
v => !state.toRemove.includes(v.id)
),
toRemove: []
};
}
case "UNDO":
return {
collection: state.collection,
toRemove: state.toRemove.filter(v => v !== action.id)
};
default:
return state;
}
}
export default function useList() {
const [state, dispatch] = useReducer(reducer, { collection: [], toRemove: [] });
useEffect(() => {
// API CALL...
dispatch({ collecion, type: "LOAD_COLLECTION" })
}, [])
const undo = (id) => {
dispatch({ id, type: "UNDO" })
}
const remove = (id) => {
dispatch({ id, type: "QUEUE_FOR_REMOVAL" });
toast(<Undo id={id} text="Removed" undo={undo} />, {
onClose: clean,
})
}
const clean = () => {
console.log("I'm being called after the toast timeout, even after unmount.")
dispatch({ type: "CLEAN_COLLECTION" })
}
return {
remove,
list: state.collection.filter((s) => !state.toRemove.includes(s.id))
}
}
I have a search component, when an input is given to the input filed and the search button is pressed I want to get a response immediately, process it and redirect to another page,
I want to get the immediate state of the store after the dispatch event
this my hook to get store state and dispatch action
export function useSpIdCheckRedirect(): [ISPPerfSummaryCardsState, SpIdData] {
const dispatch = useDispatch();
return [
useSelector<IAppstate, ISPPerfSummaryCardsState>(
(state) => state.spPerfSummaryCardsState,
shallowEqual
),
{
getSpIdData(spId: string, dataFilter: string) {
dispatch(
getDataStartSPPerfSummaryCardsAction({
spId: spId,
dateFilter: "LAST30DAYS",
})
);
},
},
];
}
this is my event handler function
const handleSPPerformanceClick = () => {
dispatch(viewTopSPPerformancePage());
};
const HandleSPSearchClick = () => {
getSpIdData(searchState, "LAST30DAYS");
console.log(state);
if (ref.current.isFetching == LoadingStatus.LOADING_SUCCESS) {
console.log(state);
}
};
this is the place where the hook is used
function SPHighlights({ spData }: Props) {
let [state, { getSpIdData }] = useSpIdCheckRedirect();
const HandleSPSearchClick = () => {
getSpIdData(searchState, "LAST30DAYS");
console.log(state);
if (state.isFetching == LoadingStatus.LOADING_SUCCESS) {
console.log(state);
}
};}
but the condition check inside the click handler becomes true only in the second click.
I want to get the store update immediately in the click handler, how can I do it?
Furthermore, I have used redux-saga middleware too.
I have a React / Redux / Meteor app in which I dispatch an action, that calls a method to get a value from the server, and the method has a callback in which I dispatch an action to save the returned value in the Redux store.
I'm also using Redux thunk.
Although my original action is only dispatched once, it runs twice. It seems that dispatching an action from inside a method callback, is causing the original action to be dispatched again.
In my React component:
class MyComponent extends Component {
....
render() {
...
}
}
function mapStateToProps(state, ownProps) {
return { value: state.myPartialState.value }
}
const Tracker = withTracker(({dispatch}) => {
const state = store.getState();
const isLoading = getIsLoading(state);
...
const handle = Meteor.subscribe('myData'), {
onReady: () => {
'onReady': () => {
secondaryPatternSubscriptions(patterns);
},
});
if (isLoading && handle.ready()) {
console.log('about to dispatch original action');
dispatch(getValue());
dispatch(setIsLoading(false));
} else if (!isLoading && !handle.ready()) {
dispatch(setIsLoading(true));
}
return { ... }
)(MyComponent);
export default connect(mapStateToProps)(Tracker);
In my actions file:
export const SET_VALUE = 'SET_VALUE';
export function setValue(value) {
return {
'type': 'SET_VALUE',
'payload': value,
};
}
export const getValue = () => (dispatch, getState) => {
console.log('about to call');
Meteor.call('getValue', (error, result) => {
console.log('about to dispatch second action');
dispatch(setValue(result)); // this causes the action to be dispatched again
});
// dispatch(setValue(10)); // this only runs once
};
const initialState = {
value: 0,
}
export default function myPartialState(state = initialState, action) {
switch (action.type) {
case SET_VALUE: {
return updeep({ 'value': action.payload }, state);
}
}
}
On the server, the method is like this:
Meteor.methods({
'getValue': function () {
...
return value;
},
})
I can see from the console logs that getValue is only dispatched once, but runs twice. I have checked this again and again, and I'm pretty near 100% sure that getValue is not dispatched twice.
I think it's something to do with calling an action from inside the method callback; if I comment out dispatch(setValue(result)); and replace it with a dispatch outside the method call, then getValue only runs once.
If I dispatch a different action instead of setValue, or change the setValue action so that it doesn't alter the 'value' property in the store, then again getValue only runs once. But I can't see why changing 'value' would cause the action to be run twice, when it is only dispatched once...
I've searched online and haven't found anything about this issue.
Can anybody think why my action is running twice, and a way to have it run only once? Thanks!
I'm trying to figure out the best way to display a sweetalert message after a successful async action. So I have an ExcursionDetail component that allows you to book the excursion. Here is the simplified component:
class ExcursionDetails extends Component {
bookExcursion() {
const userId = jwt_decode(localStorage.getItem('token')).sub;
this
.props
.actions
.bookExcursion(userId, this.props.match.params.id);
}
render() {
....
<RaisedButton label="Book Excursion" onClick={e => this.bookExcursion()}/>
....
)
}
}
const mapStateToProps = (state, ownProps) => {
return {excursion: state.excursion}
}
const mapDispatchToProps = (dispatch) => {
return {
actions: bindActionCreators(ExcursionActions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ExcursionDetails);
The action creator:
export const bookExcursion = (userId, excursionId) => {
return (dispatch, state) => {
dispatch(requestBookExcursions())
return ExcursionApi
.bookExcursion(userId, excursionId)
.then(resp => {
if (resp.ok) {
return resp
.json()
.then(payload => {
dispatch(bookExcursionsSuccess(payload.data));
})
}
}).catch(err => {
dispatch(bookExcursionsFailed(err));
})
}
}
What would be the best practice to then display the sweet alert notification? The options I thought of were:
Add a bookSuccess property that I can view if true or false in my ExcursionDetails component and if true call the sweetalert function.
Create notification specific actions and reducers and listen for it in my components. Only issue with this is I would need to have some sort of setTimeout after every notification call to clear the notification reducer and this seems a bit hacky.
call the sweet alert function within my reducer
pass a callback to the action creator
redux-thunk returns a promise; however even if the http call fails it will return a successful promise so this option doesn't seem viable.
I would and is using the first option that you mentioned.
I have created a new component and pass the redux store using connect. I check for it if the value is true on componentWillReceiveProps and set the state according and then you can display your sweetalert.
Well you can call it in the action creator.
You can use something like toastr.
Simple and clean.
export const bookExcursion = (userId, excursionId) => {
return (dispatch, state) => {
dispatch(requestBookExcursions())
return ExcursionApi
.bookExcursion(userId, excursionId)
.then(resp => {
if (resp.ok) {
return resp
.json()
.then(payload => {
dispatch(bookExcursionsSuccess(payload.data));
//Here is your notification.
toastr.success('Have fun storming the castle!', 'Miracle Max Says')
})
}
}).catch(err => {
dispatch(bookExcursionsFailed(err));
})
}
}
is it possible to dispatch an action in a reducer itself? I have a progressbar and an audio element. The goal is to update the progressbar when the time gets updated in the audio element. But I don't know where to place the ontimeupdate eventhandler, or how to dispatch an action in the callback of ontimeupdate, to update the progressbar. Here is my code:
//reducer
const initialState = {
audioElement: new AudioElement('test.mp3'),
progress: 0.0
}
initialState.audioElement.audio.ontimeupdate = () => {
console.log('progress', initialState.audioElement.currentTime/initialState.audioElement.duration);
//how to dispatch 'SET_PROGRESS_VALUE' now?
};
const audio = (state=initialState, action) => {
switch(action.type){
case 'SET_PROGRESS_VALUE':
return Object.assign({}, state, {progress: action.progress});
default: return state;
}
}
export default audio;
Starting another dispatch before your reducer is finished is an anti-pattern, because the state you received at the beginning of your reducer will not be the current application state anymore when your reducer finishes. But scheduling another dispatch from within a reducer is NOT an anti-pattern. In fact, that is what the Elm language does, and as you know Redux is an attempt to bring the Elm architecture to JavaScript.
Here is a middleware that will add the property asyncDispatch to all of your actions. When your reducer has finished and returned the new application state, asyncDispatch will trigger store.dispatch with whatever action you give to it.
// This middleware will just add the property "async dispatch" to all actions
const asyncDispatchMiddleware = store => next => action => {
let syncActivityFinished = false;
let actionQueue = [];
function flushQueue() {
actionQueue.forEach(a => store.dispatch(a)); // flush queue
actionQueue = [];
}
function asyncDispatch(asyncAction) {
actionQueue = actionQueue.concat([asyncAction]);
if (syncActivityFinished) {
flushQueue();
}
}
const actionWithAsyncDispatch =
Object.assign({}, action, { asyncDispatch });
const res = next(actionWithAsyncDispatch);
syncActivityFinished = true;
flushQueue();
return res;
};
Now your reducer can do this:
function reducer(state, action) {
switch (action.type) {
case "fetch-start":
fetch('wwww.example.com')
.then(r => r.json())
.then(r => action.asyncDispatch({ type: "fetch-response", value: r }))
return state;
case "fetch-response":
return Object.assign({}, state, { whatever: action.value });;
}
}
Dispatching an action within a reducer is an anti-pattern. Your reducer should be without side effects, simply digesting the action payload and returning a new state object. Adding listeners and dispatching actions within the reducer can lead to chained actions and other side effects.
Sounds like your initialized AudioElement class and the event listener belong within a component rather than in state. Within the event listener you can dispatch an action, which will update progress in state.
You can either initialize the AudioElement class object in a new React component or just convert that class to a React component.
class MyAudioPlayer extends React.Component {
constructor(props) {
super(props);
this.player = new AudioElement('test.mp3');
this.player.audio.ontimeupdate = this.updateProgress;
}
updateProgress () {
// Dispatch action to reducer with updated progress.
// You might want to actually send the current time and do the
// calculation from within the reducer.
this.props.updateProgressAction();
}
render () {
// Render the audio player controls, progress bar, whatever else
return <p>Progress: {this.props.progress}</p>;
}
}
class MyContainer extends React.Component {
render() {
return <MyAudioPlayer updateProgress={this.props.updateProgress} />
}
}
function mapStateToProps (state) { return {}; }
return connect(mapStateToProps, {
updateProgressAction
})(MyContainer);
Note that the updateProgressAction is automatically wrapped with dispatch so you don't need to call dispatch directly.
You might try using a library like redux-saga. It allows for a very clean way to sequence async functions, fire off actions, use delays and more. It is very powerful!
redux-loop takes a cue from Elm and provides this pattern.
Since anything is technically possible, you can do it. But you SHOULD NOT do it.
Here is a quote from Dan Abramov (the creator of Redux):
"Why would you want to dispatch inside a reducer? It's grossly
misusing the library. It's exactly the same as React doesn't allow you
to setState inside render."
From "Forbid dispatch from inside a reducer" Github ticket that he himself created
Dispatching and action inside of reducer seems occurs bug.
I made a simple counter example using useReducer which "INCREASE" is dispatched then "SUB" also does.
In the example I expected "INCREASE" is dispatched then also "SUB" does and, set cnt to -1 and then
continue "INCREASE" action to set cnt to 0, but it was -1 ("INCREASE" was ignored)
See this:
https://codesandbox.io/s/simple-react-context-example-forked-p7po7?file=/src/index.js:144-154
let listener = () => {
console.log("test");
};
const middleware = (action) => {
console.log(action);
if (action.type === "INCREASE") {
listener();
}
};
const counterReducer = (state, action) => {
middleware(action);
switch (action.type) {
case "INCREASE":
return {
...state,
cnt: state.cnt + action.payload
};
case "SUB":
return {
...state,
cnt: state.cnt - action.payload
};
default:
return state;
}
};
const Test = () => {
const { cnt, increase, substract } = useContext(CounterContext);
useEffect(() => {
listener = substract;
});
return (
<button
onClick={() => {
increase();
}}
>
{cnt}
</button>
);
};
{type: "INCREASE", payload: 1}
{type: "SUB", payload: 1}
// expected: cnt: 0
// cnt = -1