How to handle events in redux? (Specifically async ones) - javascript

Hello guys so I'm trying to handle events from async redux actions but i'm not sure how to do it correctly? In my code my AddTopping() handler calls the action creator and then the proccess cycles through. I logged everything to console.log --- based on the code below this is what the console.log will print out in order assuming the function executes fine:
success is 'pending'
success is true
It prints the initial state first which is what I don't want. I would have to call the action creator twice to actually get the 'true' for the object. I can do a setTimeout to 50 millisec and it will work but I don't want to make setTimeout's everytime. How will I handle this the right way?
Component that calls the action creator:
class Milk extends Component {
componentWillMount() {
this.AddTopping.bind(this);
}
AddTopping() {
this.props.addChocolate({TableName: 'Dark', Item: {title: 'someCake'}});
if (this.props.birthdayCake.send_success === true) {
console.log('Congrats you have added chocolate to the cake');
} else {
console.log('You have failed to add chocolate to the cake.');
}
}
render() {
return (
<div>
<h1>Chocolate is great.</h1>
</div>
);
}
}
the action creator that will then call the reducer:
export const addChocolate = (darkOrLight) => {
return ( dispatch ) => {
someAsyncFunction(darkOrLight, (err,data) => {
if (err) {
dispatch({ type: 'ADD_CHOCOLATE', success: false });
} else {
dispatch({ type: 'ADD_CHOCOLATE', success: true });
}
});
}
}
the reducer which will now update the state in the application:
const initialState = { send_success: 'pending' }
export function birthdayCake( state=initialState, action) {
switch ( action.type ) {
case ADD_CHOCOLATE:
if (action.success === true) {
return Object.assign({}, state, { send_success: true });
} else {
return Object.assign({}, state, { send_success: false });
}
default:
return state;
}
}

Related

delay on getting property, dispatch react-redux

Delay when updating the board component, my dispatch (checkResult (board)); does not work correctly. Tic-tac-toe game, here's an example of a problem:
set 3 crosses but no victory, but when I do 1 more action (set a cross or zero), then the victory is counted:
My code in:
const mapStateToProps = ({board, players}) => ({board, players});
const mapDispatchToProps = dispatch => ({
draw: (board, players, squareIndex) => {
if (!board[squareIndex]) {
if (players[players.turn] === 'X') {
dispatch(drawXAction(squareIndex));
} else {
dispatch(drawOAction(squareIndex));
}
console.log(dispatch(checkResult(board)))
dispatch(checkResult(board));
dispatch(toggleTurnAction());
}
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Square);
And checkResult func:
export function checkResult(board) {
if (checkVictory(board, 'X')) {
return {
type: X_WINS
}
} else if (checkVictory(board, '0')) {
return {
type: O_WINS
}
} else {
const check = board.filter(symbol=>symbol===null);
if(check.length===1) {
return {
type: TIE
}
}else {
return {
type: 'RANDOM'
}
}
}
}
You are passing the current board to the checkResult function, this means when your checkResult function is executing, it is not receiving the latest board -- what you have updated in the one of the previous lines.
One of the redux principle states that - it enable single source of truth. Your code is violating this principle resulting in this inconsistency. What you need to do is - get the latest state of the application in checkResult function rather than passing the board as argument.
e.g.
import store from "/path/to/store";
export function checkResult() {
// or something like this
// based on what you have in your store.
const board = store.getState().board;
// your function body
}

Vuejs - vuex computed property, DOM not updating

So I have the following code in one of my components:
export default {
name: 'section-details',
components: {
Loading
},
mounted() {
if (!this.lists.length || !this.section_types.length) {
this.$store.dispatch('section/fetch_section_form_data', () => {
if (this.section) {
this.populate_form();
}
});
}
else if (this.section) {
this.populate_form();
}
},
computed: {
section_types() {
return this.$store.state.section.section_types;
},
lists() {
return this.$store.state.list.lists;
},
loading() {
console.log(this.$store.state.section.loading);
this.$store.state.section.loading;
}
},
.
.
.
}
As you can see I have a computed property for "loading" that retrieves the attribute from my vuex store for when doing an ajax request.
in my section vuex module i have this:
fetch_section_form_data({ commit }, callback) {
commit("isLoading", true);
sectionService
.fetch_form_data()
.then((data) => {
commit("isLoading", false);
commit("fetch_section_types_success", data.section_types);
commit("list/fetch_lists_success", data.lists, { root: true});
if (callback) {
callback();
}
})
.catch((err) => {
commit("isLoading", false);
})
;
}
then in my mutations for the module i have the following code:
mutations: {
isLoading(state, status) {
state.loading = status;
},
}
Finally in my component where I store the loading property I have this:
<Loading v-if="loading"></Loading>
Anyways, for some reason the Loading component isn't showing up. the console.log in the loading() method however, is returning true for this.$store.state.section.loading. So for some reason Vue isn't picking up that loading == true in the actual DOM. Any help would be appreciated.
You need to return the value from the computed property method:
loading() {
return this.$store.state.section.loading;
}

Redux updateElementSaga has been cancelled. Why?

I just implemented a drag and drop feature with react-dnd and when the user drops the SkyElement item in my app, I update top and left on the server which in turn updates the redux store
However, the update call works occasionally, not every time. And in my console, I see a warning; updateElementSaga has been cancelled
In my SlotView.js, in a function, I have:
this.props.dispatch(requestUpdateElement({ id, top, left }));
In my elements/actions.js:
export function requestUpdateElement(element) {
return { type: 'requestUpdateElement', element };
}
In my elements/sagas.js:
export function *updateElementSaga(action) {
const response = yield call(api.updateElement, action.element);
if (response.element) {
// debugger; // this hits, saga was cancelled will have appeared in the console at this point
yield put(actions.receiveElement(response.element));
} else if (response.error) {
console.log('error receiving element');
}
}
export default [
takeLatest('requestUpdateElement', updateElementSaga),
];
In api.js:
export function updateElement(element) {
const userId = JSON.parse(localStorage.cookies).userId;
element.userId = userId;
if (userId) {
return apiHelper.put(
`${apiHelper.getBaseUrl()}/users/${element.userId}/elements/${element.id}`,
{element},
{headers: apiHelper.getHeaders()}
).catch((error) => {
return {error};
});
} else {
console.log('user ID could not be found for request');
}
}
And my elements/reducer.js:
const defaultState = {
elementsMap: {},
visibleElements: [],
unplacedElements: [],
};
export default function(state = defaultState, action) {
switch (action.type) {
case 'receiveElement':
let element = null;
let unplacedElement = null;
if (action.element.sectorId === undefined) {
unplacedElement = `${action.element.id}`;
} else {
element = `${action.element.id}`;
// don't add, duplicate
const newState = {...state}; // copy old state
delete newState[`${action.element.id}`]; // delete the item from the object
const newVisibleElements = newState.visibleElements.filter(e => e !== `${action.element.id}`); // remove item from visible elements
const newUnplacedElements = newState.unplacedElements.filter(e => e !== `${action.element.id}`);
return {
...newState,
elementsMap: {
...newState.elementsMap,
[element]: action.element,
},
visibleElements: [...newVisibleElements, element],
unplacedElements: [...newUnplacedElements],
};
}
return {
...state,
elementsMap: {
...state.elementsMap,
[action.element.id]: action.element,
},
visibleElements: [...state.visibleElements, element],
unplacedElements: [...state.unplacedElements, unplacedElement],
};
default:
return state;
}
}
Like I mentioned before, sometimes the update works, but not every time. I've narrowed the problem down to the client. Server seems to be acting fine. Any idea what I'm doing wrong here? Thanks!
If you are using takeLatest the redux saga documentation does mention:
https://redux-saga.js.org/docs/basics/UsingSagaHelpers.html
Unlike takeEvery, takeLatest allows only one fetchData task to run at
any moment. And it will be the latest started task. If a previous
task is still running when another fetchData task is started, the
previous task will be automatically cancelled.
Where fetchData is the generator function that is being served using takeLatest or takeEvery
And when your UI keeps invoking the same action, before it gets completed, it will keep cancelling
the last invoked action, and hence you would keep getting the message intermittently:
updateElementSaga has been cancelled
Which by nature takeLatest is doing the right thing. Which is:
Always take the latest invoked action
In case you want every action to be caught and processed, do use takeEvery, as:
export default [
takeEvery('requestUpdateElement', updateElementSaga),
];

React calling action results infinite loop

I have the following component. I want to run an action (this.props.selectCharacter) if condition is met. However, this results in an infinite loop currently maximum call stack exceeded
components/board.js
componentDidUpdate() {
this.props.characters.map((character) => {
// if the character is located correctly
if(character.found === true) {
// hide the overlay
document.getElementById(character.id).style.display = 'none';
// go to next character
var nextCharacter = {
id: '_x30_2-A-Kenard',
name: 'Kenard',
'avatar': 'img/2.jpg',
found: false
};
this.props.selectCharacter(nextCharacter);
}
});
}
actions/index.js
export function selectCharacter(character) {
// Action creator; needs to return an action (an object with a type property)
return {
type: 'CHARACTER_ACTIVATED',
payload: character
};
}
reducers/reducer_active_character.js
export default function(state = { id: '_x30_1-A-RussellStringerBell', name: 'Stringer Bell', avatar: 'img/1.jpg', found: false }, action) {
switch(action.type) {
case 'CHARACTER_ACTIVATED':
return action.payload;
}
return state;
}
Your issue is if one of character.found === true will update characters, which leads component update. '
console.log(characters) in your reducer, found is there a character.found === true
Change your componentDidUpdate like:
const newCharacters = character.map(c => {
return c.found ? { // new character } : c;
});
this.props.selectCharacters(newCharacters);

Unsubscribe from Redux store when condition is true?

I'm employing the suggestion from #gaearon to setup a listener on my redux store. I'm using this format:
function observeStore(store, select, onChange) {
let currentState;
if (!Function.prototype.isPrototypeOf(select)) {
select = (state) => state;
}
function handleChange() {
let nextState = select(store.getState());
if (nextState !== currentState) {
currentState = nextState;
onChange(currentState);
}
}
let unsubscribe = store.subscribe(handleChange);
handleChange();
return unsubscribe;
}
I'm using this in an onEnter handler for a react-router route:
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
const disposeRouteHandler = observeStore(store, null, (state) => {
const conditions = [
isLoaded(state.thing1),
isLoaded(state.thing2),
isLoaded(state.thing3),
];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
// I'm done: how do I dispose the store subscription???
}
});
store.dispatch(
entities.getOrCreate({
entitiesState: store.getState().entities,
nextState,
})
);
};
};
Basically this helps gate the progression of the router while actions are finishing dispatching (async).
My problem is that I can't figure out where to call disposeRouteHandler(). If I call it right after the definition, my onChange function never gets a chance to do it's thing, and I can't put it inside the onChange function because it's not defined yet.
Appears to me to be a chicken-egg problem. Would really appreciate any help/guidance/insight.
How about:
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
let shouldDispose = false;
const disposeRouteHandler = observeStore(store, null, (state) => {
const conditions = [
isLoaded(state.thing1),
isLoaded(state.thing2),
isLoaded(state.thing3),
];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
if (disposeRouteHandler) {
disposeRouteHandler();
} else {
shouldDispose = true;
}
}
});
if (shouldDispose) {
disposeRouteHandler();
}
store.dispatch(
entities.getOrCreate({
entitiesState: store.getState().entities,
nextState,
})
);
};
};
Even though using the observable pattern leads to some buy-in, you can work around any difficulties with normal js code. Alternatively you can modify your observable to suit your needs better.
For instance:
function observeStore(store, select, onChange) {
let currentState, unsubscribe;
if (!Function.prototype.isPrototypeOf(select)) {
select = (state) => state;
}
function handleChange() {
let nextState = select(store.getState());
if (nextState !== currentState) {
currentState = nextState;
onChange(currentState, unsubscribe);
}
}
unsubscribe = store.subscribe(handleChange);
handleChange();
return unsubscribe;
}
and
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
const disposeRouteHandler = observeStore(store, null, (state, disposeRouteHandler) => {
const conditions = [
isLoaded(state.thing1),
isLoaded(state.thing2),
isLoaded(state.thing3),
];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
disposeRouteHandler();
}
}
store.dispatch(
entities.getOrCreate({
entitiesState: store.getState().entities,
nextState,
})
);
};
};
It does add a strange argument to onChange but it's just one of many ways to do it.
The core problem is that handleChange gets called synchronously immediately when nothing has changed yet and asynchronously later. It's known as Zalgo.
Inspired by the suggestion from #DDS, I came up with the following alteration to the other pattern mentioned in #gaearon's comment:
export function toObservable(store) {
return {
subscribe({ onNext }) {
let dispose = this.dispose = store.subscribe(() => {
onNext.bind(this)(store.getState())
});
onNext.bind(this)(store.getState());
return { dispose };
},
dispose: function() {},
}
}
This allows me to invoke like:
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
toObservable(store).subscribe({
onNext: function onNext(state) {
const conditions = [/* many conditions */];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
this.dispose(); // remove the store subscription
}
},
});
store.dispatch(/* action */);
};
};
The key difference is that I'm passing a regular function in for onNext so as not to interfere with my bind(this) in toObservable; I couldn't figure out how to force the binding to use the context I wanted.
This solution avoids
add[ing] a strange argument to onChange
... and in my opinion also conveys a bit more intent: this.dispose() is called from within onNext, so it kinda reads like onNext.dispose(), which is exactly what I want to do.

Categories