reactjs presentation comopnent not re-rendering on redux state change - reactjs

Situation
I have a reactjs app using redux. I've got a container, and a presentation component. My presentation component renders a form with an array of simple text input fields, and I'm passing an 'onChange' callback function from the container to the presentation component which dispatches an action to change redux state when one of the text fields is typed in.
Problem
The presentation component successfully renders the correct values for the text fields from redux state when the presentation component mounts, but doesn't update when I type in the fields. If I log the props that are passed to the presentation component in mapStateToProps in the container, I can see that the onChange function is correctly dispatching to the store, and the redux state is being updated correctly. But the presentation component is not re-rendering when this happens, so typing in the text field doesn't update the view (typing does nothing).
formConnector
import { connect } from 'react-redux'
import Form from '../components/Form'
import { changeElementValue } from '../actions/actions'
const mapStateToProps = (state) => {
//e.g. state.elements = [{id:"email", value:"foo#bar.com"}]
let props = {
elements: state.elements,
}
//state and props.elements.{id}.value changes successfully when I
//type in one of the input fields, but
//the the Form component is not re-rendered
console.log(props)
return props
}
const mapDispatchToProps = (dispatch) => {
return {
onElementChange: (id, value) => {
dispatch(changeElementValue(id, value))
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Form)
Form reducer
function formReducer(state = initialState, action = null) {
switch(action.type) {
case types.CHANGE_ELEMENT_VALUE:
let newState = Object.assign({}, state)
newState.elements[action.id].value = action.value
return newState
default:
return state;
}
}
actions
import * as types from './actionTypes'
export function changeElementValue(id, value) {
return { type: types.CHANGE_ELEMENT_VALUE, id, value }
}

As discussed in the comments, this is due to state mutation.
Try to change your reducer code as follows:
case types.CHANGE_ELEMENT_VALUE: {
const newElements = state.elements.map(function(el, index) {
return action.id === index
? Object.assign({}, el, { value: action.value })
: el;
});
return Object.assign({}, state, { elements: newElements );
}
or more elegantly:
case types.CHANGE_ELEMENT_VALUE:
return { ...state, elements: state.elements.map((el, index) => (
action.id === index ? { ...el, value: action.value } : el
)}

Related

Removing a value from an array using redux toolkit

I am new to using "#reduxjs/toolkit" (version "^1.5.1").
I am trying to remove an object from within the state's array (roundScore). This is usually something that is very simple to do using filter(). For some reason this isn't working and I can't figure out why. Here's my code:
Reducer slice:
import { createSlice } from "#reduxjs/toolkit";
export const roundScoreSlice = createSlice({
name: "roundScore",
initialState: {
roundScore: [],
},
reducers: {
deleteArrow: (state, action) => {
console.log(`action.payload = ${action.payload}`); // returns correct id
state.roundScore.filter((arrow) => arrow.id !== action.payload);
},
},
});
export const { deleteArrow } = roundScoreSlice.actions;
export default roundScoreSlice.reducer;
React component:
import React from "react";
import styled from "styled-components";
import { motion } from "framer-motion";
import { useDispatch } from "react-redux";
import { deleteArrow } from "../../redux-reducers/trackSession/roundScoreSlice";
export default function InputtedScore({
arrowScore,
id,
initial,
animate,
variants,
}) {
const dispatch = useDispatch();
const applyStyling = () => {
switch (arrowScore) {
case 0:
return "miss";
case 1:
return "white";
case 2:
return "white";
case 3:
return "black";
case 4:
return "black";
case 5:
return "blue";
case 6:
return "blue";
case 7:
return "red";
case 8:
return "red";
case 9:
return "gold";
case 10:
return "gold";
default:
return null;
}
};
return (
<ParentStyled
id={id}
initial={initial}
animate={animate}
variants={variants}
onClick={() => dispatch(deleteArrow(id))}
>
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
The state after adding 2 arrows would look like this:
roundScore: [
{
id:"e0f225ba-19c2-4fd4-b2bf-1e0aef6ab4e0"
arrowScore:7
},
{
id:"2218385f-b37a-4f2c-a8db-4e7e65846171"
arrowScore:5
}
]
I've tried a combination of things.
Using e.target.id within dispatch
Using e.currentTarget.id within dispatch
Using ({id}) instead of just (id) within dispatch
Wrapping the reducer function with or without braces e.g. within (state, action) => { /* code */ }
What is it I'm missing? I know this is going to be a simple fix but for some reason it's eluding me.
Any help is much appreciated.
Okay, it looks like the issue is in the way how filter method works, it returns a new array, and an initial array is not mutated (that's why we have been using filter before to manipulate redux state), also in the code you've shown value of the filtering operation not assigned to any property of your state
You need to assign the value or mutate array, so the code below should work for you
state.roundScore = state.roundScore.filter((arrow) => arrow.id !== action.payload);
Mutate your existing array:
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);
We can think outside of the box and look at it from another way. the React component above is just actually a child of a certain parent component. And for the purpose of my answer i assume that in your parent component you have some form of array.map .
So from that code, each array item will already have an array index. and you can pass that index as a prop to the above react component like so:
const InputtedScore = ({ ...all your props, id, inputIndex }) => {
const dispatch = useDispatch();
// Having access to your index from the props, you can even
// find the item corresponding to that index
const inputAtIndex_ = useSelector(state => state.input[inputIndex])
const applyStyling = () => {
switch (arrowScore) {
...your style logic
}
};
return (
// you can send that index as a payload to the reducer function
<ParentStyled id={id} onClick={() => dispatch(deleteArrow(inputIndex))}
...the rest of your properties >
<Circle className={applyStyling()}>
{arrowScore}
<IconStyled>
<IoIosClose />
</IconStyled>
<IoIosClose className="redCross" />
</Circle>
</ParentStyled>
);
}
After dispaching the delete action by sending as payload the item's index already, you do not need to find the item in the reducer anymore:
deleteMeal: (state, action) => {
// you receive you inputIndex from the payload
let { inputIndex } = action.payload;
// and you use it to splice the desired item off the array
state.meals.splice(inputIndex, 1);
...your other logic if any
},
You need to mutate your existing array
state.roundScore.splice(state.roundScore.findIndex((arrow) => arrow.id === action.payload), 1);

Why cant I remove element from array in Reactjs with redux

I am dynamically adding <div> elements to a component by adding them to an array. This is not a problem and works well. The issue I'm trying to solve here is removing the <div> on double click by passing the id of the <div> that was doubled clicked with props when the reducer is dispatched.
The main issue is the array filter function only works when I code hard the div id both on the div and in the filter function when I want to pass the id of e.target.id on dispatch of delDiv reducer.
Note: I can remove the div successfully by changing the addDivReducer like this:
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={11} ***************************************************** Changed
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== 11; *********************************** Changed
});
return state;
But the desired effect is to pass id as props on dispatch as seen in my code below
The reducer that adds a removes elements look like this:
import DivComponent from "../../components/AddDivComponent";
const addDivReducer = (state, action) => {
switch (action.type) {
case "ADD_DIV":
return state.concat(
<DivComponent
key={Math.floor(Math.random() * 100) + 1}
id={Math.floor(Math.random() * 100) + 1}
/>
);
case "DELETE_DIV":
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
return state;
default:
return (state = []);
}
};
export default addClipartReducer;
The actions index.js look like:
export const addDiv = (props) => {
return {
type: "ADD_DIV",
payload: props,
};
};
export const deleteDiv = (props) => {
return {
type: "DELETE_DIV",
payload: props,
};
};
The delete reducer is being dispatched when the div is double clicked on like this in AddDivComponent.js:
import { useDispatch } from "react-redux";
import { deleteDiv } from "../../store/actions";
const AddDivComponent = (props) => {
const dispatch = useDispatch();
const removeClipart = (e) => {
dispatch(deleteDiv(e.target.id));
};
return(
<div
id={props.id}
className="my-div"
onDoubleClick={removeDiv}
/>
);
};
export default DivComponent;
Finally the array of <div> elements is being shown here in Canvas.js:
import { useSelector } from "react-redux";
const Canvas = () => {
const divList = useSelector((state) => state.addDIV);
return(
<div className="canvas">
{divList}
</div>
);
};
export default Canvas;
you are mutating state at your DELETE_DIV reducer. If you need to handle state, create a copy a first:
// mutating state here to a new value, can lead to problems
state = state.filter((elements) => {
return elements.props.id !== action.payload;
});
I would suggest to return filter directly, given filter already returns the desired next state, while not mutating the original:
case "DELETE_DIV":
return state.filter((elements) => {
return elements.props.id !== action.payload;
});

React top-level modal with dynamic props

I've architected my modals as suggested by Dan Abramov in this link. In short, he recommends rendering modals at the top level, and passing in the props at the same point that we dispatch an action to show the modal.
This works for the most part, but has one flaw that I cannot seem to overcome: The modal receives props only on the initial dispatch. If those props change at some point while the modal is open, those changes will not be reflected in the modal.
Here is my code:
MainScreen.js
const MainScreen = () => {
const { list, addListItem } = useListPicker()
const handleOpenModal = () => {
dispatch(modalShow('settingsModal', {
list,
addListItem,
}))
}
return (
/* ...some jsx */
<Button onPress={handleOpenModal} />
)
}
SettingsModal.js
const SettingsModal = ({ list, addListItem }) => {
return (
<List data={list} clickListItem={addListItem} />
)
}
useListPicker.js (where list is stored)
import produce from 'immer'
const initialState = {
list: [],
}
const reducer = (state, { type, ...action }) => {
switch (type) {
case 'ADD_LIST_ITEM':
return produce(state, (draft) => {
const { data } = action
draft.list.push(data)
})
default:
throw new Error('Called reducer without supported type.')
}
}
export default () => {
const [state, dispatch] = useReducer(reducer, initialState)
const addListItem = (data) => dispatch({ type: 'ADD_LIST_ITEM', data })
return {
list: state.list,
addListItem,
}
}
As we can see in SettingsModal, we can call the function passed down (addListItem) that will properly add to the list when called (this data lives in a custom hook). However, the SettingsModal has no way to get access to that updated list, since props are passed in only once: when we initially dispatch modalShow.
What is the best way to handle this issue? Would Dan's solution not be appropriate here? Before I refactored my modals to work in a similar way to his post, I rendered each modal in the relevant part of the tree. I refactored this because the same modal would appear elsewhere in the codebase.

React Native modal is not updated when Redux state is changed using hooks

I have a modal component in my React Native mobile app. It receives an array of objects from Redux state. I can delete a specific item in the array using dispatching an action using useDispatch hook. However, after sending the delete action, the component state is not updated automatically, so that I have to reopen the modal every time to see the updated list.
How can I set the modal to automatically re-render when the redux state is changed using dispatch?
SelectedItems.js
const SelectedItems = () => {
const vegetables = useSelector(state => state.new_order.vegetables)
return (
<Modal visible={isVisible}>
{vegetables.map( (v,index) =>
<VegeItem
key={index}
index={index}
name={v.name}
qty={v.qty}
metric={v.metric}
removeItem={(index) => {
dispatch({
type: 'DELETE_VEGE',
id: index
})
}}
/>)}
</View>
</Modal>
)
}
newOrderReducer.js
const newOrderReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'ADD_VEGE':
let updatedList = [...state.vegetables,action.vege]
return {
...state,
vegetables: updatedList
}
case 'DELETE_VEGE':
let newVegeList = state.vegetables
newVegeList.splice(action.id,1)
return {
...state,
vegetables: newVegeList
}
default:
return state
}
};
while doing like so let newVegeList = state.vegetables, newVegeList is just a pointer on your state and not a shallow copy of it. Therefore, you still can't mutate it as you can't mutate state outside the return part of the reducer.
so you can do like let newVegeList = [...state.vegetables], or directly at the return
return {
...state,
vegetables: state.vegetables.filter((veg, i) => i != action.id)
}
you can also send veg name or whatever and modify the checker at filter

Implementing infinite scroll with React/Redux and react-waypoint issue

Im struggling to achieve infinite scroll with my test React/Redux application.
Here how it works in simple words:
1) On componentDidMount I dispatch an action which sets the Redux state after getting 100 photos from the API. So I got photos array in Redux state.
2) I implemented react-waypoint, so when you scroll to the bottom of those photos it fires a method which dispatches another action that get more photos and "appends" them to the photos array and...
as I understand - the state changed, so redux is firing the setState and the component redraws completely, so I need to start scrolling again but its 200 photos now. When I reach waypoint again everything happens again, component fully rerenders and I need to scroll from top through 300 photos now.
This is not how I wanted it to work of course.
The simple example on react-waypoint without Redux works like this:
1) you fetch first photos and set the components initial state
2) after you scroll to the waypoint it fires a method which makes another request to the api, constructs new photos array(appending newly fetched photos) and (!) call setState with the new photos array.
And it works. No full re-renders of the component. Scroll position stays the same, and the new items appear below waypoint.
So the question is — is the problem I experience the problem with Redux state management or am I implementing my redux reducers/actions not correctly or...???
Why is setting component state in React Waypoint Infinite Scroll example (no Redux) works the way I want (no redrawing the whole component)?
I appreciate any help! Thank you!
The reducers
import { combineReducers } from 'redux';
const data = (state = {}, action) => {
if (action.type === 'PHOTOS_FETCH_DATA_SUCCESS') {
const photos = state.photos ?
[...state.photos, ...action.data.photo] :
action.data.photo;
return {
photos,
numPages: action.data.pages,
loadedAt: (new Date()).toISOString(),
};
}
return state;
};
const photosHasErrored = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
};
const photosIsLoading = (state = false, action) => {
switch (action.type) {
case 'PHOTOS_IS_LOADING':
return action.isLoading;
default:
return state;
}
};
const queryOptionsIntitial = {
taste: 0,
page: 1,
sortBy: 'interestingness-asc',
};
const queryOptions = (state = queryOptionsIntitial, action) => {
switch (action.type) {
case 'SET_TASTE':
return Object.assign({}, state, {
taste: action.taste,
});
case 'SET_SORTBY':
return Object.assign({}, state, {
sortBy: action.sortBy,
});
case 'SET_QUERY_OPTIONS':
return Object.assign({}, state, {
taste: action.taste,
page: action.page,
sortBy: action.sortBy,
});
default:
return state;
}
};
const reducers = combineReducers({
data,
photosHasErrored,
photosIsLoading,
queryOptions,
});
export default reducers;
Action creators
import tastes from '../tastes';
// Action creators
export const photosHasErrored = bool => ({
type: 'PHOTOS_HAS_ERRORED',
hasErrored: bool,
});
export const photosIsLoading = bool => ({
type: 'PHOTOS_IS_LOADING',
isLoading: bool,
});
export const photosFetchDataSuccess = data => ({
type: 'PHOTOS_FETCH_DATA_SUCCESS',
data,
});
export const setQueryOptions = (taste = 0, page, sortBy = 'interestingness-asc') => ({
type: 'SET_QUERY_OPTIONS',
taste,
page,
sortBy,
});
export const photosFetchData = (taste = 0, page = 1, sort = 'interestingness-asc', num = 500) => (dispatch) => {
dispatch(photosIsLoading(true));
dispatch(setQueryOptions(taste, page, sort));
const apiKey = '091af22a3063bac9bfd2e61147692ecd';
const url = `https://api.flickr.com/services/rest/?api_key=${apiKey}&method=flickr.photos.search&format=json&nojsoncallback=1&safe_search=1&content_type=1&per_page=${num}&page=${page}&sort=${sort}&text=${tastes[taste].keywords}`;
// console.log(url);
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(photosIsLoading(false));
return response;
})
.then(response => response.json())
.then((data) => {
// console.log('vvvvv', data.photos);
dispatch(photosFetchDataSuccess(data.photos));
})
.catch(() => dispatch(photosHasErrored(true)));
};
I also include my main component that renders the photos because I think maybe it's somehow connected with the fact that i "connect" this component to Redux store...
import React from 'react';
import injectSheet from 'react-jss';
import { connect } from 'react-redux';
import Waypoint from 'react-waypoint';
import Photo from '../Photo';
import { photosFetchData } from '../../actions';
import styles from './styles';
class Page extends React.Component {
loadMore = () => {
const { options, fetchData } = this.props;
fetchData(options.taste, options.page + 1, options.sortBy);
}
render() {
const { classes, isLoading, isErrored, data } = this.props;
const taste = 0;
const uniqueUsers = [];
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
return (
<div className={classes.wrap}>
<main className={classes.page}>
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
</main>
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}><Waypoint onEnter={() => this.loadMore()} /></div>}
{!isLoading && !isErrored && photos.length > 0 && <div className={classes.wp}>Loading...</div>}
</div>
);
}
}
const mapStateToProps = state => ({
data: state.data,
options: state.queryOptions,
hasErrored: state.photosHasErrored,
isLoading: state.photosIsLoading,
});
const mapDispatchToProps = dispatch => ({
fetchData: (taste, page, sort) => dispatch(photosFetchData(taste, page, sort)),
});
const withStore = connect(mapStateToProps, mapDispatchToProps)(Page);
export default injectSheet(styles)(withStore);
Answer to Eric Na
state.photos is an object and I just check if its present in the state. sorry, in my example I just tried to simplify things.
action.data.photo is an array for sure. Api names it so and I didn't think about renaming it.
I supplied some pics from react dev tools.
Here is my initial state after getting photos
Here is the changed state after getting new portion of photos
There were 496 photos in the initial state, and 996 after getting
additional photos for the first time after reaching waypoint
here is action.data
So all I want to say that the photos are fetched and appended but it triggers whole re-render of the component still...
I think I see the problem.
In your component you check for
{!isLoading && !isErrored && photos.length > 0 &&
photos.map(photo =>
(<Photo
key={photo.id}
taste={taste}
id={photo.id}
farm={photo.farm}
secret={photo.secret}
server={photo.server}
owner={photo.owner}
/>))
}
once you make another api request, in your action creator you set isLoading to true. this tells react to remove the whole photos component and then once it's set to false again react will show the new photos.
you need to add a loader at the bottom and not to remove the whole photos component once fetching and then render it again.
EDIT2
Try commenting out the whole uniqueUsers part (let's worry about the uniqueness of the users later)
const photos = [];
if (data.photos && data.photos.length > 0) {
data.photos.forEach((photo) => {
if (uniqueUsers.indexOf(photo.owner) === -1) {
uniqueUsers.push(photo.owner);
photos.push(photo);
}
});
}
and instead of
photos.map(photo =>
(<Photo ..
try directly mapping data.photos?
data.photos.map(photo =>
(<Photo ..
EDIT
...action.data.photo] :
action.data.photo;
can you make sure it's action.data.photo, not action.data.photos, or even just action.data? Can you try logging the data to the console?
Also,
state.photos ? .. : ..
Here, state.photos will always evaluate to true-y value, even if it's an empty array. You can change it to
state.photos.length ? .. : ..
It's hard to tell without actually seeing how you update photos in reducers and actions, but I doubt that it's the problem with how Redux manages state.
When you get new photos from ajax request, the new photos coming in should be appended to the end of the photos array in the store.
For example, if currently photos: [<Photo Z>, <Photo F>, ...] in Redux store, and the new photos in action is photos: [<Photo D>, <Photo Q>, ...], the photos in store should be updated like this:
export default function myReducer(state = initialState, action) {
switch (action.type) {
case types.RECEIVE_PHOTOS:
return {
...state,
photos: [
...state.photos,
...action.photos,
],
};
...

Resources