How to update state of specific object nested in an array - reactjs

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
})
})
}

Related

My state changes between the reducer and the consuming component

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;

How to map through objects and arrays in translations

I have 2 JSON files with my translation data -> EN file and FR file.
I am using react-i18next to handle my translations this way, which works fine, but since I am having repeatable components I need to map/loop through the translations to get the right output
Example
en:
export const EN = {
page1: {
section1 {
titles: {
title1: "title1_name",
title2: "title2_name",
title3: "title3_name"
},
buttons: {
button1 : "button1_name",
button2 : "button2_name",
button3 : "button3_name",
}
}
section2 { THE SAME AS SECTION 1 }
section3 { THE SAME AS SECTION 1 }
page2 { SAME AS PAGE 1 }
The same thing applies for FR file (with french translations instead)
How can achieve to mapping all e.g titles from section1 and page1. Would that be even correct to use map() ?
Right now my solution is just to use {t page1.section1.0.titles.title1} which of course print the same title everywhere - in this case title1_name
Current Output with using {t page1.section1.0.titles.title1} :
slider1: title1_name
slider2: title1_name
slider3: title1_name
slider4: title1_name
and so on so on...
Expected output:
slider1: title1_name, button1_name
slider2: title2_name, button2_name
slider3: title4_name, button3_name
slider4: title4_name, button4_name
This works, you'll need to do the translation, but this gives you access to the title object in each page and section:
Object.entries(EN).map(([key,object]) => {
Object.entries(object).map(([token, value]) => {
console.log(`${token} : ${value}`);
Object.keys(value).map((key, index) => {
console.log(value[key]); // titles access here
});
});
});
When you are iterating over an object you'll want to use a function that gets data from your object in an array. Traditionally that is Object.keys(), but newer versions of Javascript introduced Object.values() and Object.entries() which can be very helpful depending on the situation. You can access a value from its key like myObject[myKey] so Object.keys() can work in every situation.
The current structure of your JSON file is not ideal because you have totally separate objects for titles and buttons so you can't ensure that you have the same amount of title texts as button texts. I'll address this in a moment, but first here is one way that you can use the current structure.
const MySlider = () => {
const currentlang = ... // get the EN or FR object based on your current language
// an array of all the title texts
const titles = Object.values(currentlang.page1.section1.titles);
// an array of all the button texts
const buttons = Object.values(currentlang.page1.section1.buttons);
return (
<Slider>
{titles.map((title, i) => ( // map through the titles
<Slide
title={title}
buttonText={buttons[i]} // get the button text with the same index -- how do we know this is valid?
key={i} // i as a key is not great
/>
))}
</Slider>
);
};
With some dummy components so that you can render it:
const Slider: React.FC = ({ children }) => <div>{children}</div>;
interface SliderProps {
title: string;
buttonText: string;
}
const Slide: React.FC<SliderProps> = ({ title, buttonText }) => {
return (
<div>
<h2>{title}</h2>
<button>{buttonText}</button>
</div>
);
};
I would recommend grouping the labels by slide rather than by titles and buttons. This ensures that titles and buttons match up, allows easy access by the slide key if you want to customize the order, and gives us a unique key property for our components.
export const EN = {
page1: {
section1: {
slides: {
sale: {
title: "title1_name",
button: "button1_name"
},
featured: {
title: "title2_name",
button: "button2_name"
},
promo: {
title: "title3_name",
button: "button3_name"
},
}
}
}
};
const MySlider = () => {
const currentlang = ... // get the EN or FR object based on your current language
// keyed object of slides
const slides = currentlang.page1.section1.slides;
return (
<Slider>
{Object.entries(slides).map(
// Object.entries gives us an array with elements key and value
([key, value]) => (
<Slide
title={value.title}
buttonText={value.button}
key={key}
/>
)
)}
</Slider>
);
};
Key-based approach to allow custom ordering:
const MySlider = () => {
const currentlang = ... // get the EN or FR object based on your current language
// keyed object of slides
const slides = currentlang.page1.section1.slides;
// helper function renders the slide for a given key
const renderSlide = (key: keyof typeof slides) => {
const {title, button} = slides[key];
return (
<Slide
title={title}
buttonText={button}
/>
)
}
return (
<Slider>
{renderSlide("promo")}
{renderSlide("sale")}
{renderSlide("featured")}
</Slider>
);
};
There is no option for a .map functions on objects but you can use the Object.keys Option.:
var myObject = { 'a': 1, 'b': 2, 'c': 3 };
Object.keys(myObject).map(function(key, index) {
myObject[key] *= 2;
});
console.log(myObject);
// => { 'a': 2, 'b': 4, 'c': 6 }
map function for objects (instead of arrays)
Medium MultiLanguage Website

How can I display three random exercises from an array every time a button is pressed using React?

I'm trying to display a list of 3 random exercises every time you press a button and change those 3 exercises on each button press
it chooses a random workout correctly in the console, but I'm not sure how to make it display and update correctly using React.
how can I make it so it displays a random 3 exercises every time the button is pressed. I've seen ways of doing it using only JS but am curious how to do this with React.
import React, { Component } from "react";
class Workout extends Component {
render() {
const upperExercises = [
"test1",
"test2",
"test3"
]
const lowerExercises = [
"test4",
"test5",
"test6"
]
const coreExercises = [
"test7",
"test8",
"test9"
]
function getAll() {
let randomUpper = upperExercises[Math.floor(Math.random() * upperExercises.length)];
console.log(randomUpper)
let randomLower = lowerExercises[Math.floor(Math.random() * lowerExercises.length)];
console.log(randomLower)
let randomCore = coreExercises[Math.floor(Math.random() * coreExercises.length)];
console.log(randomCore)
}
return (
<section>
<button onClick={() => { getAll() }}>TEST</button>
<p>DISPLAY OUTPUT</p>
</section>
)
}
}
export default Workout
You can try something like this. You can randomly get the values from your arrays and then setState of the values so you can access them easily in your <p/> tags.
I changed your getAll function to an arrow function to avoid having to bind to this . It's more simpler for beginners to use Es6 arrow functions. Let me know if this worked . Good Luck!!
import React, { Component } from "react"
const upperExercises = [
"test1",
"test2",
"test3"
]
const lowerExercises = [
"test4",
"test5",
"test6"
]
const coreExercises = [
"test7",
"test8",
"test9"
]
class Workout extends Component {
constructor(props) {
super(props)
//initialise state
this.state = {
lower ='',
upper ='',
core ='',
}
}
//Changed your function to arrow function to avoid binding and its
// a simplier way of doing it
getAll = () => {
const randomUpper = Math.floor(Math.random() * upperExercises.length);
const upper = upperExercises[randomUpper]
//Get the random value and set the state to that value
this.setState({
upper: upper
})
const randomLower = Math.floor(Math.random() * lowerExercises.length);
const lower = lowerExercises[randomLower]
this.setState({
lower: lower
})
const randomCore = Math.floor(Math.random() * randomCore.length);
const core = coreExercises[randomCore]
this.setState({
core: core
})
}
render() {
return (
<section>
<button onClick={this.getAll}>TEST</button>
<p>Upper {this.state.upper}</p>
<p>Lower {this.state.lower}</p>
<p>Core {this.state.core}</p>
</section>
)
}
}
export default Workout

How to update the whole app due to specific event?

I build a 3D product configurator and a problem appeared. I am pretty new to React wherefore I can't figure out how to solve it.
My app structure is as follows:
App.js
- Scene3D
- Options
- Scene3D
- CheckoutBar
All changes can be made by the user by clicking different buttons in the option section. One of them is to change the product model. Each product model provides different options. All options are stored in an object in App.js.
My thought was to create an event "change-model" which triggers the render function of App.js again with the choosen model. Unfortunately this doesn't work. The option section won't be updated. The new option data won't get passed to all component underlying App.js.
How is it possible to solve that?
I appreciate your help. Thank you!
App.js
constructor(props) {
super(props)
this.state = {
configurations: {
standard: require('./configurations/standard.json'),
second: require('./configurations/standard.json'),
third: require('./configurations/third.json')
}
}
}
componentDidMount() {
window.addEventListener('change-model', this.render)
this.loadScripts()
}
render = e => {
return (
<div className="proconf">
<Scene3d defaultConfig={this.getDefaultConfig()} />
<Controls configuration={this.state.configurations[e && e.detail.model ? e.detail.model : "standard"]} />
<CheckoutBar defaultConfig={this.getDefaultConfig()} prices={this.getPrices()} />
</div>
)
}
Controls.js
emitModelChangeEvent(modelName) {
let event = new CustomEvent('change-model', { detail: {
model: modelName
}})
window.dispatchEvent(event)
}
createOptions = (options) => {
let optionStorage = []
for (var x in options) {
this.emitModelChangeEvent(value)
this.setState(prev => ({
productConfig: {
...prev.productConfig,
model: value
}
}))
let buttonStorage = []
for (var y in options[x].values) {
buttonStorage.push(<div onClick={(e) => { e.preventDefault(); emitChangeEvent(this, valueKey) }}>{valueInfo}</div>)
}
optionStorage.push(<div>{buttonStorage}</div>)
return optionStorage
}
render() {
return (
<div>
<button>
{this.props.stepName}
</button>
<div>
{this.createOptions(this.props.options)}
</div>
</div>
)
}

Retain state data on re render in reactjs

I cannot share the whole code as it is very complex.
The app is basically a todo list task in which we can add main task and subtasks corresponding to main tasks
Structure is like this
Main task 1
1.
2.
3.
Now i maintain an index variable in my json structure and when send my data to the child component as
this.state.Todo[index].items
Now the problem i am facing is
when i add the first task ie this.state.Todo[0].items and its subtasks then it works fine
But if i add second task this.state.Todo1.items then child componen only gets data for second index and subtasks of first child that i previously added disappears from the page.
Main task 1
//Disappears
Main task 2
1.
2.
3.
I am not able to figure it out how to solve this issue
UPDATE:
here is my json
{
Todo:[
{name:"primary",
items:[
{
item:'',
isDone:false,
cost:0}
]
}
],
filter:[
{
keyword:'',
Status:"SHOW_ALL"
}
],
selectedCatelog:"0"};
**starting Component todoAdd.js**
var React=require('react');
var ReactDOM=require('react-dom');
import TodoBanner from './TodoComponents/TodoBanner.js';
import TodoForm from './TodoComponents/TodoForm.js';
import TodoList from './TodoComponents/TodoList.js';
import TodoCost from './TodoComponents/TodoCost.js';
var TodoApp = React.createClass({
getInitialState : function(){
return {Todo:[{name:"primary",items:[{item:'',isDone:false,cost:0}
]}],filter:[{keyword:'',Status:"SHOW_ALL"}],selectedCatelog:"0"};
},
updateItems: function(newItem){
var item = {item:newItem.item,isDone:false,cost:newItem.cost};
var newtodo = this.state.Todo;
var allItems = this.state.Todo[this.state.selectedCatelog].items.concat([item]);
newtodo[this.state.selectedCatelog].items = allItems;
this.setState({
Todo: newtodo
});
},
deleteItem : function(index){
var newtodo = this.state.Todo;
var allItems = this.state.Todo[this.state.selectedCatelog].items.slice(); //copy array
allItems.splice(index, 1); //remove element
newtodo[this.state.selectedCatelog].items = allItems;
this.setState({
Todo: newtodo
});
},
filterItem : function(e){
this.state.filter[0].Status = e.target.value;
this.setState({
filter: this.state.filter
});
},
searchItem : function(e){
this.state.filter[0].keyword = e.target.value;
this.setState({
filter: this.state.filter
});
},
AddCatalog: function(newCatalog){
var Catalog = {name:newCatalog,items:[]};
var newtodo = this.state.Todo.concat([Catalog]);
this.setState({
Todo: newtodo
});
},
setID:function(index)
{
if(index-this.state.selectedCatelog===1)
{
this.setState({
selectedCatelog: index
});
}
},
setSelectedCatalog: function(index){
},
setIndexItem:function(index)
{
this.setState({
selectedCatelog: index
});
},
render: function(){
console.log(JSON.stringify(this.state.Todo))
const AppBarExampleIcon = () => (
<AppBar title="Module Cost Estimation" />
);
return (
<div>
<div>
<MuiThemeProvider>
<AppBarExampleIcon />
</MuiThemeProvider>
<TodoCost cost={this.state.Todo}/>
<ToDoCatalogForm onFormSubmit = {this.AddCatalog} />
<ToDoCatelog setItemId={this.setIndexItem} set={this.updateItems} onSelectDemo={this.setID} selectedID = {this.state.selectedCatelog} onSelected={this.setSelectedCatalog} Todos = {this.state.Todo} />
</div>
<div>
<TodoList ListIndex={this.state.selectedCatelog} items = {this.state.Todo} filter = {this.state.filter} onDelete={this.deleteItem}/>
<ToDoFilter onFilter = {this.filterItem} onSearch = {this.searchItem} filter={this.state.filter}/>
</div>
</div>
);
}
});
ReactDOM.render(<TodoApp />, document.getElementById("app"));
When i Add a task its name gets added in name property of Json and when i enter subtasks then subtasks are populated in items property corressponding to the name and i am updating the index using selectedCatelog.Whenever a new task is added selectedCatelog is updated in setIndexItem method and then i pass entire json to TodoList Component
Here is the screenshot before adding second main task
Here is the screenshot after adding second main task
For the few information you give us it looks like the problem is updating the state.
When you update a member of the state you overwrite that member with the new one, so you have to take the information you had before and add the new one.
Besides, you can't directly mutate the state like this.state.Todo.push(newTodo), so being Todo an array you should use a method like concat to add a new element as it returns a new Array.
const { Todo } = this.state;
let newTodo = {"name":'sec',"items":[2,4,5,7]}
this.setState({ Todo:Todo.concat([newTodo])});

Resources