My state changes between the reducer and the consuming component - reactjs

App purpose: The purpose of this React app is to handle scoring of a very specific dart-game. 2 players, each having to reach 33 hits in the fields 20-13, Tpl's, Dbls's and Bulls. No points, only the number of hits are counted. The hits are added manually by the players (no automatiion required :)).
Each targetfield has a row of targets and 2 buttons for adding and removing a hit of that target field.
I have implemented the useContext-design for maintaining state, which looks like this:
export interface IMickeyMouseGameState {
player1 : IPlayer | null,
player2 : IPlayer | null,
winner : IPlayer | null,
TotalRounds : number,
GameStatus: Status
CurrentRound: number
}
Other objects are designed like this :
export interface IGame {
player1?:IPlayer;
player2?:IPlayer;
sets: number;
gameover:boolean;
winner:IPlayer|undefined;
}
export interface IPlayer {
id:number;
name: string;
targets: ITarget[];
wonSets: number;
hitRequirement : number
}
export interface ITarget {
id:number,
value:string,
count:number
}
export interface IHit{
playerid:number,
targetid:number
}
So far so good.
This is the reducer action with the signature:
export interface HitPlayerTarget {
type: ActionType.HitPlayerTarget,
payload:IHit
}
const newTargets = (action.payload.playerid === 1 ? [...state.player1!.targets] : [...state.player2!.targets]);
const hitTarget = newTargets.find(tg => {
return tg.id === action.payload.targetid;
});
if (hitTarget) {
const newTarget = {...hitTarget}
newTarget.count = hitTarget.count-1;
newTargets.splice(newTargets.indexOf(hitTarget),1);
newTargets.push(newTarget);
}
if (action.payload.playerid === 1) {
state.player1!.targets = [...newTargets];
}
if (action.payload.playerid === 2) {
state.player2!.targets = [...newTargets];
}
let newState: IMickeyMouseGameState = {
...state,
player1: {
...state.player1!,
targets: [...state.player1!.targets]
},
player2: {
...state.player2!,
targets: [...state.player2!.targets]
}
}
return newState;
In the Main component i instantiate the useReducerHook:
const MickeyMouse: React.FC = () => {
const [state, dispatch] = useReducer(mickeyMousGameReducer, initialMickeyMouseGameState);
const p1Props: IUserInputProps = {
color: "green",
placeholdertext: "Angiv Grøn spiller/hold",
iconSize: 24,
playerId: 1,
}
const p2Props: IUserInputProps = {
playerId: 2,
color: "red",
placeholdertext: "Angiv Rød spiller/hold",
iconSize: 24,
}
return (
<MickyMouseContext.Provider value={{ state, dispatch }} >
<div className="row mt-3 mb-5">
<h1 className="text-success text-center">Mickey Mouse Game</h1>
</div>
<MickeyMouseGameSettings />
<div className="row justify-content-start">
<div className="col-5">
{state.player1 ?<UserTargetList playerid={1} /> : <UserInput {...p1Props} /> }
</div>
<div className="col-1 bg-dark text-warning rounded border border-warning">
<MickeyMouseLegend />
</div>
<div className="col-5">
{state.player2 ? <UserTargetList playerid={2} /> : <UserInput {...p2Props} /> }
</div>
</div>
</MickyMouseContext.Provider>
);
}
export default MickeyMouse;
Now the reducer-action correctly subtracts 1 from the target's count (the point is to get each target count to 0 and the new state correctly shows the target with 1 less than the old state, but when the Consumer (in this case a tsx-component called UserTargets, which is respnsible for rendering each target with either a circle or an X) the state of the target is 2 lower, even though the reducer only subtracted 1....
After adding a single hit to player 'Peter' in the 20-field - the rendering (with consoloe-logs) looks like this:
So I guess my question is this : Why is the state mutating between the reducer and the consumer and what can I do to fix it?
If further explanation is needed, please ask, if this question should be simplpified, please let me know...
I usually don't ask questions here - I mostly find anwers.
The project i available on github: https://github.com/martinmoesby/dart-games

Issue
I suspect a state mutation in your reducer case is being exposed by the React.StrictMode.
StrictMode - Detecting unexpected side effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <--
The function being the reducer function.
const newTargets = (action.payload.playerid === 1 // <-- new array reference OK
? [...state.player1!.targets]
: [...state.player2!.targets]);
const hitTarget = newTargets.find(tg => {
return tg.id === action.payload.targetid;
});
if (hitTarget) {
const newTarget = { ...hitTarget }; // <-- new object reference OK
newTarget.count = hitTarget.count - 1; // <-- new property OK
newTargets.splice(newTargets.indexOf(hitTarget), 1); // <-- inplace mutation but OK since newTargets is new array
newTargets.push(newTarget); // <-- same
}
if (action.payload.playerid === 1) {
state.player1!.targets = [...newTargets]; // <-- state.player1!.targets mutation!
}
if (action.payload.playerid === 2) {
state.player2!.targets = [...newTargets]; // <-- state.player2!.targets mutation!
}
let newState: IMickeyMouseGameState = {
...state,
player1: {
...state.player1!,
targets: [...state.player1!.targets] // <-- copies mutation
},
player2: {
...state.player2!,
targets: [...state.player2!.targets] // <-- copies mutation
}
}
return newState;
state.player1!.targets = [...newTargets]; mutates and copies in the update into the previous state.player1 state and when the reducer is run again, a second update mutates and copies in the update again.
Solution
Apply immutable update patterns. Shallow copy all state the is being updated.
const newTargets = (action.payload.playerid === 1
? [...state.player1!.targets]
: [...state.player2!.targets]);
const hitTarget = newTargets.find(tg => tg.id === action.payload.targetid);
if (hitTarget) {
const newTarget = {
...hitTarget,
count: hitTarget.count - 1,
};
newTargets.splice(newTargets.indexOf(hitTarget), 1);
newTargets.push(newTarget);
}
const newState: IMickeyMouseGameState = { ...state }; // shallow copy
if (action.payload.playerid === 1) {
newState.player1 = {
...newState.player1!, // shallow copy
targets: newTargets,
};
}
if (action.payload.playerid === 2) {
newState.player1 = {
...newState.player2!, // shallow copy
targets: newTargets,
};
}
return newState;

Related

State changed in context provider not saved

So I'm trying to centralize some alert-related logic in my app in a single .tsx file, that needs to be available in many components (specfically, an "add alert" fuction that will be called from many components). To this end I am trying to use react context to make the alert logic available, with the state (an array of active alerts) stored in App.tsx.
Alerts.tsx
export interface AlertContext {
alerts: Array<AppAlert>,
addAlert: (msg: React.ReactNode, style: string, callback?: (id: string) => {}) => void,
clearAlert: (id: string) => void
}
[...]
export function AlertsProvider(props: AlertsProps) {
function clearAlert(id: string){
let timeout = props.currentAlerts.find(t => t.id === id)?.timeout;
if(timeout){
clearTimeout(timeout);
}
let newCurrent = props.currentAlerts.filter(t => t.id != id);
props.setCurrentAlerts(newCurrent);
}
function addAlert(msg: JSX.Element, style: string, callback: (id: string) => {}) {
console.log("add alert triggered");
let id = uuidv4();
let newTimeout = setTimeout(clearAlert, timeoutMilliseconds, id);
let newAlert = {
id: id,
msg: msg,
style: style,
callback: callback,
timeout: newTimeout
} as AppAlert;
let test = [...props.currentAlerts, newAlert];
console.log(test);
props.setCurrentAlerts(test);
console.log("current alerts", props.currentAlerts);
}
let test = {
alerts: props.currentAlerts,
addAlert: addAlert,
clearAlert: clearAlert
} as AlertContext;
return (<AlertsContext.Provider value={test}>
{ props.children }
</AlertsContext.Provider>);
}
App.tsx
function App(props: AppProps){
[...]
const [currentAlerts, setCurrentAlerts] = useState<Array<AppAlert>>([]);
[...]
const alertsContext = useContext(AlertsContext);
console.log("render app", alertsContext.alerts);
return (
<AlertsProvider currentAlerts={currentAlerts} setCurrentAlerts={setCurrentAlerts}>
<div className={ "app-container " + (error !== undefined ? "err" : "") } >
{ selectedMode === "Current" &&
<CurrentItems {...currentItemsProps} />
}
{ selectedMode === "History" &&
<History {...historyProps } />
}
{ selectedMode === "Configure" &&
<Configure {...globalProps} />
}
</div>
<div className="footer-container">
{
alertsContext.alerts.map(a => (
<Alert variant={a.style} dismissible transition={false} onClose={a.callback}>
{a.msg}
</Alert>
))
}
{/*<Alert variant="danger" dismissible transition={false}
show={ error !== undefined }
onClose={ dismissErrorAlert }>
<span>{ error?.msg }</span>
</Alert>*/}
</div>
</AlertsProvider>
);
}
export default App;
I'm calling alertsContext.addAlert in only one place in CurrentItems.tsx so far. I've also added in some console statements for easier debugging. The output in the console is as follows:
render app Array [] App.tsx:116
XHRGEThttp://localhost:49153/currentitems?view=Error [HTTP/1.1 500 Internal Server Error 1ms]
Error 500 fetching current items for view Error: Internal Server Error CurrentItems.tsx:94
add alert triggered Alerts.tsx:42
Array [ {…}, {…} ] Alerts.tsx:53
current alerts Array [ {…} ] Alerts.tsx:55
render app Array []
So I can see that by the end of the addAlert function the currentAlerts property appears to have been updated, but then subsequent console statement in the App.tsx shows it as empty. I'm relatively new to React, so I'm probably having some misunderstanding of how state is meant to be used / function, but I've been poking at this on and off for most of a day with no success, so I'm hoping someone can set me straight.
const alertsContext = useContext(AlertsContext);
This line in App is going to look for a provider higher up the component tree. There's a provider inside of App, but that doesn't matter. Since there's no provider higher in the component tree, App is getting the default value, which never changes.
You will either need to invert the order of your components, so the provider is higher than the component that's trying to map over the value, or since the state variable is already in App you could just use that directly and delete the call to useContext:
function App(props: AppProps){
[...]
const [currentAlerts, setCurrentAlerts] = useState<Array<AppAlert>>([]);
[...]
// Delete this line
// const alertsContext = useContext(AlertsContext);
console.log("render app", currentAlerts);
[...]
{
currentAlerts.map(a => (
<Alert variant={a.style} dismissible transition={false} onClose={a.callback}>
{a.msg}
</Alert>
))
}
}

Setting state with React Context from child component

Hey y'all I am trying to use the Context API to manage state to render a badge where the it's not possible to pass props. Currently I am trying to use the setUnreadNotif setter, but it seems because I am using it in a method that loops through an array that it is not working as expected. I have been successful updating the boolean when only calling setUnreadNotif(true/false); alone so I know it works. I have tried many other approaches unsuccessfully and this seems the most straight forward. My provider is wrapping app appropriately as well so I know its not that. Any help is greatly appreciated.
Here is my Context
import React, {
createContext,
Dispatch,
SetStateAction,
useContext,
useState,
} from 'react';
import { getContentCards } from 'ThisProject/src/utils/braze';
import { ContentCard } from 'react-native-appboy-sdk';
export interface NotificationsContextValue {
unreadNotif: boolean;
setUnreadNotif: Dispatch<SetStateAction<boolean>>;
}
export const defaultNotificationsContextValue: NotificationsContextValue = {
unreadNotif: false,
setUnreadNotif: (prevState: SetStateAction<boolean>) => prevState,
};
const NotificationsContext = createContext<NotificationsContextValue>(
defaultNotificationsContextValue,
);
function NotificationsProvider<T>({ children }: React.PropsWithChildren<T>) {
const [unreadNotif, setUnreadNotif] = useState<boolean>(false);
return (
<NotificationsContext.Provider
value={{
unreadNotif,
setUnreadNotif,
}}>
{children}
</NotificationsContext.Provider>
);
}
function useNotifications(): NotificationsContextValue {
const context = useContext(NotificationsContext);
if (context === undefined) {
throw new Error('useUser must be used within NotificationsContext');
}
return context;
}
export { NotificationsContext, NotificationsProvider, useNotifications };
Child Component
export default function NotificationsPage({
navigation,
}: {
navigation: NavigationProp<StackParamList>;
}) {
const [notificationCards, setNotificationCards] = useState<
ExtendedContentCard[]
>([]);
const user = useUser();
const { setUnreadNotif } = useNotifications();
const getCards = (url: string) => {
if (url.includes('thisproject:')) {
Linking.openURL(url);
} else {
navigation.navigate(ScreenIdentifier.NotificationsStack.id, {
screen: ScreenIdentifier.NotificationsWebView.id,
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
params: {
uri: `${getTrustedWebAppUrl()}${url}`,
title: 'Profile',
},
});
}
getContentCards((response: ContentCard[]) => {
response.forEach((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
});
});
Braze.requestContentCardsRefresh();
};
return (
<ScrollView style={styles.container}>
<View style={styles.contentContainer}>
{notificationCards?.map((item: ExtendedContentCard) => {
return (
<NotificationCard
onPress={getCards}
key={item.id}
id={item.id}
title={item.title}
description={item.cardDescription}
image={item.image}
clicked={item.clicked}
ctaTitle={item.domain}
url={item.url}
/>
);
})}
</View>
</ScrollView>
);
}
Fixed Issue
I was able to fix the issue by foregoing the forEach and using a findIndex instead like so:
getContentCards((response: ContentCard[]) => {
response.findIndex((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
setUnreadNotif(false);
});
});
An issue I see in the getContentCards handler is that it is mis-using the Array.prototype.findIndex method to issue unintended side-effects, the effect here being enqueueing a state update.
getContentCards((response: ContentCard[]) => {
response.findIndex((card) => {
if (card.clicked === false) {
setUnreadNotif(true);
}
setUnreadNotif(false);
});
});
What's worse is that because the passed predicate function, e.g. the callback, never returns true, so each and every element in the response array is iterated and a state update is enqueued and only the enqueued state update when card.clicked === false evaluates true is the unreadNotif state set true, all other enqueued updates set it false. It may be true that the condition is true for an element, but if it isn't the last element of the array then any subsequent iteration is going to enqueue an update and set unreadNotif back to false.
The gist it seems is that you want to set the unreadNotif true if there is some element with a falsey card.clicked value.
getContentCards((response: ContentCard[]) => {
setUnreadNotif(response.some(card => !card.clicked));
});
Here you'll see that the Array.prototype.some method returns a boolean if any of the array elements return true from the predicate function.
The some() method tests whether at least one element in the array
passes the test implemented by the provided function. It returns true
if, in the array, it finds an element for which the provided function
returns true; otherwise it returns false. It doesn't modify the array.
So long as there is some card in the response that has not been clicked, the state will be set true, otherwise it is set to false.

React useReducer bug while updating state array

I haven't been in React for a while and now I am revising. Well I faced error and tried debugging it for about 2hours and couldn't find bug. Well, the main logic of program goes like this:
There is one main context with cart object.
Main property is cart array where I store all products
If I add product with same name (I don't compare it with id's right now because it is small project for revising) it should just sum up old amount of that product with new amount
Well, I did all logic for adding but the problem started when I found out that for some reason when I continue adding products, it linearly doubles it up. I will leave github link here if you want to check full aplication. Also, there I will leave only important components. Maybe there is small mistake which I forget to consider. Also I removed logic for summing up amount of same products because that's not neccesary right now. Pushing into state array is important.
Github: https://github.com/AndNijaz/practice-react-
//Context
import React, { useEffect, useReducer, useState } from "react";
const CartContext = React.createContext({
cart: [],
totalAmount: 0,
totalPrice: 0,
addToCart: () => {},
setTotalAmount: () => {},
setTotalPrice: () => {},
});
const cartAction = (state, action) => {
const foodObject = action.value;
const arr = [];
console.log(state.foodArr);
if (action.type === "ADD_TO_CART") {
arr.push(foodObject);
state.foodArr = [...state.foodArr, ...arr];
return { ...state };
}
return { ...state };
};
export const CartContextProvider = (props) => {
const [cartState, setCartState] = useReducer(cartAction, {
foodArr: [],
totalAmount: 0,
totalPrice: 0,
});
const addToCart = (foodObj) => {
setCartState({ type: "ADD_TO_CART", value: foodObj });
};
return (
<CartContext.Provider
value={{
cart: cartState.foodArr,
totalAmount: cartState.totalAmount,
totalPrice: cartState.totalAmount,
addToCart: addToCart,
}}
>
{props.children}
</CartContext.Provider>
);
};
export default CartContext;
//Food.js
import React, { useContext, useState, useRef, useEffect } from "react";
import CartContext from "../../context/cart-context";
import Button from "../ui/Button";
import style from "./Food.module.css";
const Food = (props) => {
const ctx = useContext(CartContext);
const foodObj = props.value;
const amountInput = useRef();
const onClickHandler = () => {
const obj = {
name: foodObj.name,
description: foodObj.description,
price: foodObj.price,
value: +amountInput.current.value,
};
console.log(obj);
ctx.addToCart(obj);
};
return (
<div className={style["food"]}>
<div className={style["food__info"]}>
<p>{foodObj.name}</p>
<p>{foodObj.description}</p>
<p>{foodObj.price}$</p>
</div>
<div className={style["food__form"]}>
<div className={style["food__form-row"]}>
<p>Amount</p>
<input type="number" min="0" ref={amountInput} />
</div>
<Button type="button" onClick={onClickHandler}>
+Add
</Button>
</div>
</div>
);
};
export default Food;
//Button
import style from "./Button.module.css";
const Button = (props) => {
return (
<button
type={props.type}
className={style["button"]}
onClick={props.onClick}
>
{props.children}
</button>
);
};
export default Button;
Issue
The React.StrictMode component is exposing an unintentional side-effect.
See Detecting Unexpected Side Effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer <-- here
The function passed to useReducer is double invoked.
const cartAction = (state, action) => {
const foodObject = action.value;
const arr = [];
console.log(state.foodArr);
if (action.type === "ADD_TO_CART") {
arr.push(foodObject); // <-- mutates arr array, pushes duplicates!
state.foodArr = [...state.foodArr, ...arr]; // <-- duplicates copied
return { ...state };
}
return { ...state };
};
Solution
Reducer functions are to be considered pure functions, taking the current state and an action and compute the next state. In the sense of pure functionality, the same next state should result from the same current state and action. The solution is only add the new foodObject object once, based on the current state.
Note also for the default "case" just return the current state object. Shallow copying the state without changing any data will unnecessarily trigger rerenders.
I suggest also renaming the reducer function to cartReducer so its purpose is more clear to future readers of your code.
const cartReducer = (state, action) => {
switch(action.type) {
case "ADD_TO_CART":
const foodObject = action.value;
return {
...state, // shallow copy current state into new state object
foodArr: [
...state.foodArr, // shallow copy current food array
foodObject, // append new food object
],
};
default:
return state;
}
};
...
useReducer(cartReducer, initialState);
Additional Suggestions
When adding an item to the cart, first check if the cart already contains that item, and if so, shallow copy the cart and the matching item and update the item's value property which appears to be the quantity.
Cart/item totals are generally computed values from existing state. As such these are considered derived state and they don't belong in state, these should computed when rendering. See Identify the minimal (but complete) representation of UI state. They can be memoized in the cart context if necessary.

React-component is not re-rendered when the store is changed, neither automatically nor even by force update

This functional component should display a sorted list with checkboxes at each item that change the values in the store.
For some reason it is not re-rendered when the store is changed. And without a re-renderer, it (and the whole application) works very crookedly and halfway. I suspect that this is because the store object remains the same, albeit with new content. But I don’t understand how to fix it. I have even inserted a force update to the checkbox handler, but for some reason it does not work too.
Component:
import React, { useState, useReducer } from 'react';
import { ReactSortable } from 'react-sortablejs';
import ListItem from '#mui/material/ListItem';
import Checkbox from '#mui/material/Checkbox';
import { connect } from 'react-redux';
import { setGameVisible, setGameInvisible } from '../store/actions/games';
interface IGamesListProps {
games: [];
setGameVisible: (id: string) => void;
setGameInvisible: (id: string) => void;
}
interface ItemType {
id: string;
name: string;
isVisible: boolean;
}
const GamesList: React.FunctionComponent<IGamesListProps> = ({games, setGameVisible, setGameInvisible}) => {
const [state, setState] = useState<ItemType[]>(games);
// eslint-disable-next-line
const [ignored, forceUpdate] = useReducer(x => x + 1, 0); // this way of force updating is taken from the official React documentation (but even it doesn't work!)
const onCheckboxChangeHandle = (id: string, isVisible: boolean) => {
isVisible ? setGameInvisible(id) : setGameVisible(id);
forceUpdate(); // doesn't work :(((
}
return (
<ReactSortable list={state} setList={setState} tag='ul'>
{state.map((item) => (
<ListItem
sx={{ maxWidth: '300px' }}
key={item.id}
secondaryAction={
<Checkbox
edge="end"
onChange={() => onCheckboxChangeHandle(item.id, item.isVisible)}
checked={item.isVisible}
/>
}
>
{item.name}
</ListItem>
))}
</ReactSortable>
);
};
export default connect(null, { setGameVisible, setGameInvisible })(GamesList);
Reducer:
import { SET_GAMES, SET_GAME_VISIBLE, SET_GAME_INVISIBLE } from '../actions/games';
export const initialState = {
games: [],
};
export default function games(state = initialState, action) {
switch(action.type) {
case SET_GAMES: {
for(let obj of action.payload.games) {
obj.isVisible = true;
}
return {
...state,
games: action.payload.games,
};
}
case SET_GAME_VISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = true;
};
}
return {
...state,
};
}
case SET_GAME_INVISIBLE: {
for(let obj of state.games) {
if (obj.id === action.payload.id) {
obj.isVisible = false;
};
}
return {
...state,
};
}
default:
return state;
}
}
Thank you for any help!
Note: By the information You gave I came with the idea of the problem, but I posted here because it is going to be explanatory and long.
First of all, you don't pass the new game via mapStateToProps into Component in a state change, and even you do, useState won't use new game prop value for non-first render. You must use useEffect and trigger changes of the game and set the to state locally.
At this point you must find the inner state redundant and you can remove it and totally rely on the redux state.
const mapStateToProp = (state) => ({
games: state.games // you may need to change the path
})
connect(mapStateToProp, { setGameVisible, setGameInvisible })(GamesList);
Second, the reducer you made, changes the individual game item but not the games list itself. because it is nested and the reference check by default is done as strict equality reference check-in redux state === state. This probably doesn't cause an issue because the outer state changes by the way, but I think it worth it to mention it.
for(let obj of action.payload.games) {
obj.isVisible = true; // mutating actions.payload.games[<item>]
}
return {
...state,
games: [...action.payload.games], // add immutability for re-redenr
};
// or use map
return {
...state,
games: action.payload.games.map(obj => ({...obj, isVisible:true})),
};
Third, It's true your forceUpdate will cause the component to re-render, and you can test that by adding a console.log, but it won't repaint the whole subtree of your component including inner children if their props don't change that's because of performance issue. React try to update as efficiently as possible. Also you the key optimization layer which prevent change if the order of items and id of them doesn't change

How to update state of specific object nested in an array

I have an array of objects. I want my function clicked() to add a new parameter to my object (visible: false). I'm not sure how to tell react to update my state for a specific key without re-creating the entire array of objects.
First of all, is there an efficient way to do this (i.e using the spread operator)?
And second of all, perhaps my entire structure is off. I just want to click my element, then have it receive a prop indicating that it should no longer be visible. Can someone please suggest an alternative approach, if needed?
import React, { Component } from 'react';
import { DefaultButton, CompoundButton } from 'office-ui-fabric-react/lib/Button';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import OilSite from './components/oilsite';
import './index.css';
class App extends Component {
constructor(props){
super(props);
this.state = {
mySites: [
{
text: "Oil Site 1",
secondaryText:"Fracking",
key: 3
},
{
text: "Oil Site 2",
secondaryText:"Fracking",
key: 88
},
{
text: "Oil Site 3",
secondaryText:"Fracking",
key: 12
},
{
text: "Oil Site 4",
secondaryText:"Fracking",
key: 9
}
],
}
};
clicked = (key) => {
// HOW DO I DO THIS?
}
render = () => (
<div className="wraper">
<div className="oilsites">
{this.state.mySites.map((x)=>(
<OilSite {...x} onClick={()=>this.clicked(x.key)}/>
))}
</div>
</div>
)
};
export default App;
Like this:
clicked = (key) => {
this.state(prevState => {
// find index of element
const indexOfElement = prevState.mySites.findIndex(s => s.key === key);
if(indexOfElement > -1) {
// if element exists copy the array...
const sitesCopy = [...prevState.mySites];
// ...and update the object
sitesCopy[indexOfElement].visible = false;
return { mySites: sitesCopy }
}
// there was no element with a given key so we don't update anything
})
}
You can use the index of the array to do a O(1) (No iteration needed) lookup, get the site from the array, add the property to the object, update the array and then set the state with the array. Remeber, map has 3 parameters that can be used (value, index, array).
UPDATE: Fixed Some Typos
class Site
{
constructor(text, scdText, key, visible=true)
{
this.text = text;
this.secondaryText = scdText;
this.key = key;
this.isVisible = visible;
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
mySites: [
new Site("Oil Site 1", "Fracking", 3),
new Site("Oil Site 2", "Fracking", 88),
new Site("Oil Site 3", "Fracking", 12),
new Site("Oil Site 4", "Fracking", 9)
],
}
this.clicked = this.clicked.bind(this);
};
//Change to a normal function
clicked(ind)
{
//Get the sites from state
let stateCopy = {...this.state}
let {mySites} = stateCopy;
let oilSite = mySites[ind]; //Get site by index
//Add property to site
oilSite.isVisible = false;
mySites[ind] = oilSite;//update array
//Update the state
this.setState(stateCopy);
}
render = () => (
<div className="wraper">
<div className="oilsites">
{this.state.mySites.map((site, ind) => (
//Add another parameter to map, index
<OilSite {...site} onClick={() => this.clicked(ind)} />
))}
</div>
</div>
)
};
I'm not sure how to tell react to update my state for a specific key without re-creating the entire array of objects.
The idea in react is to return a new state object instead of mutating old one.
From react docs on setstate,
prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props
You can use map and return a new array.
clicked = (key) => {
this.setState({
mySites: this.state.mySites.map(val=>{
return val.key === key ? {...val, visibility: false} : val
})
})
}

Resources