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);
Related
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
}
I am trying to get some statistics and problems for a user using a Redux action and pass it to a React component. The problem is, I have the array of objects curPageExercisesMarked, which I use for the pagination of the page, but it does not take the values I assign it to.
The stranger thing is that the other fields in the Redux store get updated, but not this one. I tried consoling the object in the action, but it just prints this:
It is important to mention that I am doing something similar in another action, using the exact same assignment and it works there. I've lost already an hour trying to figure this thing out so any help is welcomed.
The Redux action:
export const setStatistics = (
problems,
problemsSolved,
filter = ''
) => dispatch => {
let payload = {
subject1: 0,
subject2: 0,
subject3: 0,
total: 0,
exercisesMarked: [],
curPageExercisesMarked: []
};
for (let i = 0; i < problems.length; i++) {
if (problems[i].S === '1' && problemsSolved.includes(problems[i]._id)) {
payload.subject1++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
} else if (
problems[i].S === '2' &&
problemsSolved.includes(problems[i]._id)
) {
payload.subject2++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
} else if (
problems[i].S === '3' &&
problemsSolved.includes(problems[i]._id)
) {
payload.subject3++;
payload.total++;
payload.exercisesMarked.push(problems[i]);
}
}
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
dispatch({
type: SET_USER_STATISTICS,
payload
});
};
The redux reducer:
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_USER_STATISTICS:
return {
...state,
exercisesMarked: payload.exercisesMarked,
curPageExercisesMarked: payload.curPageExercisesMarked,
subject1: payload.subject1,
subject2: payload.subject2,
subject3: payload.subject3,
total: payload.total
};
case CHANGE_PAGE_MARKED:
return {
...state,
page: payload,
curPageExercisesMarked: state.exercisesMarked.slice(
(payload - 1) * state.pages_count,
payload * state.pages_count
)
};
default:
return state;
}
}
This is the part that does not function:
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
EDIT
I've discovered that if I go a component which loads all the problems and come back to this component, it actually gets the correct value.
Now, the interesting is that I do get the same problems here as well. Is it the way I use React Hook?
This is the part where I call the redux action in the react component:
const Dashboard = ({
problems: { problems },
auth: { user },
getProblems,
dashboard: {
curPageExercisesMarked,
page,
exercisesMarked,
pages_count,
subject1,
subject2,
subject3,
total
},
setStatistics
}) => {
useEffect(() => {
if (problems === null) {
getProblems();
} else if (user !== null) {
setStatistics(problems, user.problemsSolved);
}
}, [problems, user]);
// rest of the code
}
You can first simplify code as below. Update/Print console.log(JSON.stringify(payload)). I think if(problemsSolved.includes(problems[i]._id)) not working as expected
export const setStatistics = (
problems,
problemsSolved,
filter = ""
) => dispatch => {
let payload = {
subject1: 0,
subject2: 0,
subject3: 0,
total: 0,
exercisesMarked: [],
curPageExercisesMarked: []
};
for (let i = 0; i < problems.length; i++) {
if(problemsSolved.includes(problems[i]._id)) {
payload["subject"+ problems[i].S]++
payload.total++;
payload.exercisesMarked.push(problems[i]);
}
}
payload.curPageExercisesMarked = payload.exercisesMarked.slice(0, 10);
dispatch({
type: SET_USER_STATISTICS,
payload
});
};
// Also
case SET_USER_STATISTICS:
return {
...state,
...payload
};
I have a question regarding preventing duplicates from being added to my redux store.
It should be straight forward but for some reason nothing I try is working.
export const eventReducer = (state = [], action) => {
switch(action.type) {
case "ADD_EVENT":
return [...state, action.event].filter(ev => {
if(ev.event_id !== action.event.event_id){
return ev;
}
});
default:
return state;
}
};
The action looks something like the below:
{
type: "ADD_EVENT",
event: { event_id: 1, name: "Chelsea v Arsenal" }
}
The issue is that on occasions the API I am working with is sending over identical messages through a websocket, which means that two identical events are getting added to my store.
I have taken many approaches but cannot figure out how to get this to work. I have tried many SO answers,
Why your code is failing?
Code:
return [...state, action.event].filter(ev => {
if(ev.event_id !== action.event.event_id){
return ev;
}
});
Because first you are adding the new element then filtering the same element, by this way it will never add the new value in the reducer state.
Solution:
Use #array.findIndex to check whether item already exist in array or not if not then only add the element otherwise return the same state.
Write it like this:
case "ADD_EVENT":
let index = state.findIndex(el => el.event_id == action.event.event_id);
if(index == -1)
return [...state, action.event];
return state;
You can use Array.prototype.find().
Example (Not tested)
const eventExists = (events, event) => {
return evets.find((e) => e.event_id === event.event_id);
}
export const eventReducer = (state = [], action) = > {
switch (action.type) {
case "ADD_EVENT":
if (eventExists(state, action.event)) {
return state;
} else {
return [...state, action.event];
}
default:
return state;
}
};
Update (#CodingIntrigue's comment)
You can also use Array.prototype.some() for a better approach
const eventExists = (events, event) => {
return evets.some((e) => e.event_id === event.event_id);
}
export const eventReducer = (state = [], action) = > {
switch (action.type) {
case "ADD_EVENT":
if (eventExists(state, action.event)) {
return state;
} else {
return [...state, action.event];
}
default:
return state;
}
};
Solution:
const eventReducer = ( state = [], action ) => {
switch (action.type) {
case 'ADD_EVENT':
return state.some(( { event_id } ) => event_id === action.event.event_id)
? state
: [...state, action.event];
default:
return state;
}
};
Test:
const state1 = eventReducer([], {
type: 'ADD_EVENT',
event: { event_id: 1, name: 'Chelsea v Arsenal' }
});
const state2 = eventReducer(state1, {
type: 'ADD_EVENT',
event: { event_id: 2, name: 'Chelsea v Manchester' }
});
const state3 = eventReducer(state2, {
type: 'ADD_EVENT',
event: { event_id: 1, name: 'Chelsea v Arsenal' }
});
console.log(state1, state2, state3);
You can something like this, for the logic part to ensure you don't get the same entry twice.
const x = filter.arrayOfData(item => item.event_id !== EVENT_FROM_SOCKET);
if (x.length === 0) {
// dispatch action here
} else {
// ignore and do nothing
}
You need to be careful when using Arrays in reducers. You are essentially adding more items to the list when you call:
[...state, action.event]
If you instead use a map then you can prevent duplicates
const events = { ...state.events }
events[action.event.event_id] = action.event.name]
{...state, events }
If duplicate exist in previous state then we should return same state else update the state
case "ADDPREVIEW":
let index = state.preview.findIndex(dup => dup.id == action.payload.id);
return {
...state,
preview: index == -1 ? [...state.preview,action.payload]:[...state.preview]
};
My store looks like this,
{
name: "john",
foo: {},
arr: [
{
id:101,
desc:'comment'
},
{
id:101,
desc:'comment2'
}
]
}
My textarea looks like this
<textarea
id={arr.id} //"101"
name={`tesc:`}
value={this.props.store.desc}
onChange={this.props.onChng}
/>
My action is
export const onChng = (desc) => ({
type: Constants.SET_DESC,
payload: {
desc
}
});
My reducer
case Constants.SET_DESC:
return update(state, {
store: {
streams: {
desc: { $set: action.payload.desc }
}
}
});
It works only if arry is an object, I had to make changes to the stream to an array and I am confused how I can update to an array, also how does get the right value from the store.
The following example taken from the redux documentation might help you in the use case how to update items in an array. For more on this you can read on here http://redux.js.org/docs/recipes/StructuringReducers.html
state structure is something like this
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
and reducer code is like below
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
function appReducer(state = initialState, action) {
switch(action.type) {
case 'EDIT_TODO' : {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, {text : action.text});
});
return updateObject(state, {todos : newTodos});
}
default : return state;
}
}
If you have to update an element in a array within your store you have to copy the array and clone the matching element to apply your changes.
So in the first step your action should contain either the already cloned (and changed) object or the id of the object and the properties to change.
Here is a rough example:
export class MyActions {
static readonly UPDATE_ITEM = 'My.Action.UPDATE_ITEM';
static updateItem(id: string, changedValues: any) {
return { type: MyActions.UPDATE_ITEM, payload: { id, changedValues } };
}
}
export const myReducer: Reducer<IAppState> = (state: IAppState = initialState, action: AnyAction): IAppState => {
switch (action.type) {
case MyActions.UPDATE_ITEM:
return { ...state, items: merge(state.items, action.payload) };
default:
return state;
}
}
const merge = (array, change) => {
// check if an item with the id already exists
const index = array.findIndex(item => item.id === change.id);
// copy the source array
array = [...array];
if(index >= 0) {
// clone and change the existing item
const existingItem = array[index];
array[index] = { ...existingItem, ...change.changedValues };
} else {
// add a new item to the array
array.push = { id: change.id, ...change.changedValues };
}
return array;
}
To update an array, I would use immutability helper and do something like this - to your reducer
let store = {"state" : {
"data": [{
"subset": [{
"id": 1
}, {
"id": 2
}]
}, {
"subset": [{
"id": 10
}, {
"id": 11
}, {
"id": 12
}]
}]
}}
case Constants.SET_DESC:
return update(store, {
"state" : {
"data": {
[action.indexToUpdate]: {
"subset": {
$set: action.payload.desc
}
}
}
}
})
});
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;
}
}