Property doesn't update after firing redux dispatch - reactjs

After hours I have to ask for help, my useEffect doesn't always work on reduxs dispatching action.
const ScreenA = { currentItem, updatedItem } => {
useEffect(() => {
if (currentItem.id === updatedItem.id) { // Do stuff }
}, [updatedItem]) // XX Is not changing
... // do stuf
const mapStateToProps = (state) => {
console.log("AAABB ScreenA.mapStateToProps updatedItem: ", state.updatedItem) // XX I receive new updated id!! but it doesn't change the prop.updatedItem for ScreenA and useEffect is not calling.
return {
updatedItem: state.updatedItem,
}
}
export default connect(mapStateToProps)(FeedScreen)
}
My Reducer
Update
const initialState = {
updatedItem: undefined,
}
export default (state = initialState, action) => {
switch (action.type) {
case ITEM_CHANGED:
console.log("AAABB reducer.addChangedItem.ITEM_CHANGED: " + action.t) // Is printing after every dispatch call
return {
...state,
updatedItem: action.updatedItem,
}
// I tried also with
return {
updatedItem: action.updatedItem,
}
default:
return state
}
}
Strange was that it works if I dispatch change action from ScreenB to ScreenA but not between ScreenC to ScreenA. (It was the same way how I dispatch the action.

Spread out your updated item as well
Since your updated item is an object perhaps that causes a mutation. Seems unlikely but this are the issues you face when not using flat data. also generally you have a type and a payload (not updated item) on your action in redux.
return {
...state,
updatedItem: {...action.updatedItem},
}
If this doesn't work then probably something in your useEffect is broken, you need to log inside there and show results.

Related

How to update a component according to the state?

I need to display a list of objects in my Explorer component.
In my app I use useReducer hook what wrapped by Context.
It works well when the data flow is in "input-mode" (when I update data in state). But it does not rerender the application after data was changed.
So, steps that I need to pass and get a positive result.
Press btn with file icon (or folder icon). This btn call hook that take a look into state and make a decision: where this file or folder should be placed in my simple fs.
Write file/folder name (this function doesn't exist yet).
Apply name by press enter or click mouse out of input (the same as 2 step).
Currently, I try to create file/folder with hardcoded name for testing 1-step. And I expect that dispatch function pass data to the state and it would be updated and rerendered. All process runs well except of rerender.
I explain the flow of 1-st step.
After I click btn, I call the func from hook for forming my instance.
Then, the new instance saving into local useState.
After local useState successfully was updated, I call dispatch in useEffect hook.
In reducer I modify my state and return it.
After this steps I expect that my app will automatically rerendered, but it isn't.
Code snippets, step by step.
First step.
const handleFileClick = () => {
formAnInstance('file');
console.log('file btn click')
};
Second step.
// in useInstancesInteraction hook
const { state, dispatch } = useStateContext();
const [instance, setInstance] = useState<IInstance>();
const formAnInstance = (mode: Omit<Mode, 'root'>) => {
if (
typeof state?.currentFolder === 'undefined' ||
state?.currentFolder === null
) {
const target =
mode === 'folder'
? (createInstance('folder', 'folder') as IInstance)
: (createInstance('file', 'file') as IInstance);
target['..'] = '/';
setInstance(target);
}
};
Third step.
// in useInstancesInteraction hook
useEffect(() => {
const updateData = () => {
if (dispatch && instance) {
dispatch(createRootInstance(instance));
}
};
updateData();
}, [instance]);
Fourth step.
export const initialState = {
root: createInstance('/', 'root') as IInstance,
currentFolder: null,
};
const reducer = (state = initialState, action: IAction) => {
const { type, payload } = action;
switch (type) {
case ACTION_TYPES.CREATE_ROOT_INSTANCE:
const myKey = payload['.'];
Object.assign(state.root, { [myKey]: payload });
console.log('Reducer', state?.root);
return state;
case ACTION_TYPES.CREATE_FILE:
break;
case ACTION_TYPES.UPLOAD_FILE:
break;
case ACTION_TYPES.RENAME_FILE:
break;
case ACTION_TYPES.DELETE_FILE:
break;
case ACTION_TYPES.CREATE_FOLDER:
break;
case ACTION_TYPES.RENAME_FOLDER:
break;
case ACTION_TYPES.DELETE_FOLDER:
break;
default:
return state;
}
};
Here how my context file look like:
import React, { useContext, useReducer } from 'react';
import { IContext } from './index.types';
import reducer, { initialState } from './reducer';
const StateContext = React.createContext<IContext>({
state: undefined,
dispatch: null,
});
const StateProvider = ({
children,
}: {
children: JSX.Element | JSX.Element[];
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={{ state, dispatch }}>
{children}
</StateContext.Provider>
);
};
export default StateProvider;
export const useStateContext = () => useContext(StateContext);

Redux Thunk Submit to Action

I been stuck to this quite a bit, I am trying to pass in my state to the redux but it seems like I am doing it wrong.
This are my code:
This is my submit function
popForm() {
let states = this.state.orders;
let d = states.filter((data) => {
return data !== null && data !== undefined
});
// console.log("d",d);
this.props.LogInClick(d);
// LogInClick(state);
}
This is my mapToDispatch
const mapDispatchToProps = (dispatch) => {
return {
LogInClick : (data) => dispatch(Actions.addDynamic(data)),
}
}
Action call
export const addDynamic = ({data}) => {
console.log("Manage to get to here");
console.log("dataInAction",data);
}
My reducer
case Actions.ADD_DYNAMIC: {
return {
...state,
data: action.payload
};
}
Your synchronous action should to return an object with type and payload.
When dealing with async actions, you need thunk(or saga etc) middleware. Your code seem to dispatch normal action (not async). So just make sure that your action returns type and payload.
Like this
export const addDynamic = ({data}) => {
console.log("Manage to get to here");
console.log("dataInAction",data);
return {
type: Action.ADD_DYNAMIC,
payload: data
}
}

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!

Saving realtime listener in redux

I need to trigger firestore realtime listener on login to listen to user profile data changes and cancel it before logout. To do that I need to save realtime listener in the store where I get stuck. I'm trying to do this in redux
export const cancelListener = (cancelListener) => {
return {
type: actionTypes.CANCEL_LISTENER,
cancelListener: cancelListener
}
}
export const uDataListener = (uid) => {
return dispatch => {
dispatch(uDataStart())
const dbRef = db.collection("user").doc(uid)
const cancelSubscription = dbRef
.onSnapshot(
(doc) => {
dispatch(uDataSuccess(doc.data()))
}
, ((error) => {
dispatch(uDataFail(error.message))})
);
dispatch(cancelListener(cancelSubscription))
}
}
and on logout simply call it from the redux store
export const logout = (cancelListener) => {
cancelListener()
fire.auth().signOut()
return {
type: actionTypes.AUTH_LOGOUT
}
}
However nothing is being saved in cancelListener therefore it can not be triggered. How do I accomplish this task? Please
Thanks
I have woken up in the middle of the night with other idea. I tried to add the method in the constant in action instead of saving the method in the redux state or reducer. I'm not sure if this is the best approach but it does the job. Now I just don't understand why I didn't try this approach in the first place. Here is the code which will need a bit of tweaks yet but it works
let cancelListener = null
export const logout = () => {
cancelListener()
fire.auth().signOut()
return {
type: actionTypes.AUTH_LOGOUT
}
}
export const auth = (email, password) => {
return dispatch => {
dispatch(authStart())
fire.auth().signInWithEmailAndPassword(email, password).then((u) => {
dispatch(authSuccess(u.user))
const dbRef = db.collection("user").doc(u.user.uid)
cancelListener = dbRef.onSnapshot((doc) => {
dispatch(saveUserData(doc.data()))
})
}).catch((error) => {
dispatch(authFailed(error.message))
});
}
}
Thank you very much for your help anyway. I really appreciate that
Just a quick thought, in uDataListener call an action e.g. START_LISTENER and in reducer you can have:
import { store } from './yourStore';
let cancelListener, dbRef;
function reducer(state, action) {
switch (action.type) {
case "START_LISTENER":
dbRef = db.collection("user").doc(action.uid)
cancelSubscription = dbRef.onSnapshot(function(doc) {
store.dispatch(
yourAction(doc.data()); //Dispatch new action using store
)
})
return state;
case "STOP_LISTENER":
cancelListener()
return state;
default:
return state;
}
STOP_LISTENER will be dispached when you are doing logout
Below you can see link how to dispatch from outside a component
Update React component by dispatching action from non-react component

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