Get the immediate redux store state inside click event handler function - reactjs

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.

Related

How do I get the state from useSelector after dispatch is done?

I have a FormCheck component that calls the handleChange function on the onChange event. Task: change the state of the checkbox, and send a request to the server with the new state. The problem is that the submission is done later, i.e. the getSelectedCategories function is executed before the submission.
const data = useSelector((state) => state.products);
const boxes = getBoxes(data);
const handleChange = (id) => {
dispatch(setFilterCategory(id));
const selectedCategories = getSelectedCategories(data);
console.log(selectedCategories); // This is done before the dispatch while the state changes
dispatch(fetchProducts(selectedCategories);
};
return (
{boxes.map(item => {
return <FormCheck label={item.value} checked={item.isChecked} onChange={() => handleChange(item.id)}
})}
);
By definition, callbacks defined in the body of a function component can only access state, props, and values that existed when the callback was created at the time the component rendered.
If you're dispatching an action, it's impossible for that code to access the new Redux state (or even React state) on the next line.
If you do need to access the new Redux state immediately, you can do that via a thunk, which has access to getState:
const updateCategoryAndFetch = (category) => {
return (dispatch, getState) => {
dispatch(setFilterCategory(id));
const selectedCategories = getSelectedCategories(getState());
dispatch(fetchProducts(selectedCategories)
}
}
// later, in the component:
const handleChange = (id) => {
dispatch(updateCategoryAndFetch(id));
}
This is what useEffect() is made for:
const data = useSelector((state) => state.products);
const selectedCategories = getSelectedCategories(data);
const handleChange = (id) => {
dispatch(setFilterCategory(id));
};
// is executed whenever selectedCategories changes
useEffect(() => {
dispatch(fetchProducts(selectedCategories);
}, [selectedCategories])

How to stop component re-render when redux store change

When I dispatch an action from a child component that triggers a change in the Redux store, the whole tree gets re-render including the child component itself. How can I stop that?
For example ...
1- I'm listening for when a user clicks on a child component to expand it.
const [expanded, setExpanded] = useState(false);
<span onClick={() => setExpanded(!expanded)}>Expand</span>
2- Then if expanded is true I dispatch an action in useEffect ...
useEffect(() => {
if (expanded) {
dispatch(asyncGetItems())
}
}, [expanded]);
3- Then inside the async dispatch it changes the redux store based on the returned data ...
export const getItems = (items: {}): AppActions => {
return {
type: GET_ITEMS,
payload: items,
};
};
export const asyncGetItems = () => {
return async (dispatch: any, getState: () => AppState) => {
try {
const { data } = await apiGetItems();
dispatch(getItems(data));
} catch (error) {
handleResponseError(error);
}
};
};
Now, when I do that and change the data in the store the component gets re-rendered because its parents get re-rendered, how to stop that?
I tried to use memo like this, but it didn't work ...
export default memo(MyComponent);
And when I don't change anything in the store and just return the data like this ...
export const asyncGetItems = () => {
return async (dispatch: any, getState: () => AppState) => {
try {
const { data } = await apiGetItems();
return data;
} catch (error) {
handleResponseError(error);
}
};
};
The component doesn't get re-rendered.

React state is not updated after dispatch call using React useContext hook

Yow guys, React beginner here.
So basically, I am trying to fetch the updated state using React useContext hook.
The state is set inside a function call where the dispatch is placed, and the function call is bind to the button onClick event.
The function where the dispatch is called:
const fetchLocation = async (id: number) => {
const [, result] = await getLatestLocation()
dispatch({
type: "LOCATION_FETCHED",
payload: result
})
console.log(result) //this prints the latest/updated location even on button first click
}
Reducer:
case "LOCATION_FETCHED": {
return {
...state,
location: payload,
}
}
The function call in the component:
const {
fetchLocation,
location
} = React.useContext(locationContext)
const [fetchingLocation, setFetchingLocation] = useState(false)
const getLocation = (id: number) => {
fetchLocation(id)
.then(() => {
setFetchingLocation(true)
})
.finally(() => {
setFetchingLocation(false)
console.log(location) //this prints nothing or empty on the first button click
})
}
Button onClick function bind:
onClick={() => getLocation(143)}
I'm not sure what is happening, the first click will log nothing but on the second click, I got the updated location state.
As the comments say, the dispatch works asynchronously. So if you want to know the new value you should use the useEffect hook like this.
useEffect(() => {
console.log(location)
}, [location])
You can read more about here: https://reactjs.org/docs/hooks-effect.html

React / Redux / Meteor: method that dispatches an action in the callback, is called twice

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!

Can I dispatch an action in reducer?

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

Resources