How to map through objects and arrays in translations - reactjs

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

Related

How can animate between array filter methods with React?

Im currently building my personal Portfolio and I have an array of objects containing multiple projects, each one of the project object contain a property called category, which can have a value of 'Visual' or 'Complexity'.
My plan is to add two buttons, one for each category, but I would like to animate between the category changes, a simple fade out and fade in would be nice, any idea how can I do this ?
I tried using React Transition Group for this, but didn't manage to figure it out.
The first Component is the ProjectUl, this component will receive a filter prop, which is basically a string indicating which category should the filter method use.
const projects = [
{
name: "Limitless",
category: "visual",
dash: "- Landing Page",
img: imgList.limitless,
gif: imgList.limitlessGif,
},
{
name: "Spacejet",
category: "complexity visual",
dash: "- Landing Page / API Project; ",
img: imgList.spacejet,
},
];
export const ProjectsUl: FC<IProps> = ({ filter }) => {
const [filtered, setFiltered] = useState<IProject[]>([])
useEffect(() => {
const filteredProjects = projects.filter((i) => i.category.includes(filter));
setFiltered(filteredProjects);
}, [filter]);
return (
<ProjectsUlStyle>
{filtered.map((i) => (
<ProjectsLi filtered={i} />
))}
</ProjectsUlStyle>
);
};
Then, inside the ProjectsUl there will be the ProjectsLi, which are the list of projects, here is the code for the ProjectsLi
export const ProjectsLi: FC<any> = ({ filtered }) => {
return (
<LiStyle>
<img src={filtered.img} />
<div>
<span className="dash">{filtered.dash}</span>
<header>
<h1>{filtered.name}</h1>
<FAicons.FaGithub />
<FAicons.FaCode />
</header>
</div>
</LiStyle>
);
};

SolidJS: input field loses focus when typing

I have a newbie question on SolidJS. I have an array with objects, like a to-do list. I render this as a list with input fields to edit one of the properties in these objects. When typing in one of the input fields, the input directly loses focus though.
How can I prevent the inputs to lose focus when typing?
Here is a CodeSandbox example demonstrating the issue: https://codesandbox.io/s/6s8y2x?file=/src/main.tsx
Here is the source code demonstrating the issue:
import { render } from "solid-js/web";
import { createSignal, For } from 'solid-js'
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<For each={todos()}>
{(todo, index) => {
console.log('render', index(), todo)
return <div>
<input
value={todo.text}
onInput={event => {
setTodos(todos => {
return replace(todos, index(), {
...todo,
text: event.target.value
})
})
}}
/>
</div>
}}
</For>
Data: {JSON.stringify(todos())}
</div>
</div>
);
}
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T) : Array<T> {
const clone = array.slice(0)
clone[index] = newItem
return clone
}
render(() => <App />, document.getElementById("app")!);
UPDATE: I've worked out a CodeSandbox example with the problem and the three proposed solutions (based on two answers): https://codesandbox.io/s/solidjs-input-field-loses-focus-when-typing-itttzy?file=/src/App.tsx
<For> components keys items of the input array by the reference.
When you are updating a todo item inside todos with replace, you are creating a brand new object. Solid then treats the new object as a completely unrelated item, and creates a fresh HTML element for it.
You can use createStore instead, and update only the single property of your todo object, without changing the reference to it.
const [todos, setTodos] = createStore([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
const updateTodo = (id, text) => {
setTodos(o => o.id === id, "text", text)
}
Or use an alternative Control Flow component for mapping the input array, that takes an explicit key property:
https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#Key
<Key each={todos()} by="id">
...
</Key>
While #thetarnav solutions work, I want to propose my own.
I would solve it by using <Index>
import { render } from "solid-js/web";
import { createSignal, Index } from "solid-js";
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T): Array<T> {
const clone = array.slice(0);
clone[index] = newItem;
return clone;
}
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<Index each={todos()}>
{(todo, index) => {
console.log("render", index, todo());
return (
<div>
<input
value={todo().text}
onInput={(event) => {
setTodos((todos) => {
return replace(todos, index, {
...todo(),
text: event.target.value
});
});
}}
/>
</div>
);
}}
</Index>
Dat: {JSON.stringify(todos())}
</div>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
As you can see, instead of the index being a function/signal, now the object is. This allows the framework to replace the value of the textbox inline.
To remember how it works: For remembers your objects by reference. If your objects switch places then the same object can be reused. Index remembers your values by index. If the value at a certain index is changed then that is reflected in the signal.
This solution is not more or less correct than the other one proposed, but I feel this is more in line and closer to the core of Solid.
With For, whole element will be re-created when the item updates. You lose focus when you update the item because the element (input) with the focus gets destroyed, along with its parent (li), and a new element is created.
You have two options. You can either manually take focus when the new element is created or have a finer reactivity where element is kept while the property is updated. The indexArray provides the latter out of the box.
The indexArray keeps the element references while updating the item. The Index component uses indexArray under the hood.
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<ul>
{indexArray(todos, (todo, index) => (
<li>
<input
value={todo().text}
onInput={(event) => {
const text = event.target.value;
setTodos(todos().map((v, i) => i === index ? { ...v, text } : v))
}}
/>
</li>
))}
</ul>
);
}
Note: For component caches the items internally to avoid unnecessary re-renders. Unchanged items will be re-used but updated ones will be re-created.

Takes two clicks for react bootstrap popover to show up

I've run into an issue while trying to build a page that allows the user to click on a word and get its definition in a bootstrap popover. That is achieved by sending an API request and updating the state with the received data.
The problem is that the popover only appears after the second click on the word. The console.log() in useEffect() shows that every time a new word is clicked an API request is made. For the popover to appear the same word must be clicked twice. It'd be better if it only took one click.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
export default function App() {
const [text, setText] = useState(
"He looked at her and saw her eyes luminous with pity."
);
const [selectedWord, setSelectedWord] = useState("luminous");
const [apiData, setApiData] = useState([
{
word: "",
phonetics: [{ text: "" }],
meanings: [{ definitions: [{ definition: "", example: "" }] }]
}
]);
const words = text.split(/ /g);
useEffect(() => {
var url = "https://api.dictionaryapi.dev/api/v2/entries/en/" + selectedWord;
axios
.get(url)
.then(response => {
setApiData(response.data)
console.log("api call")
})
.catch(function (error) {
if (error) {
console.log("Error", error.message);
}
});
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const popover = (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</Popover.Body>
</Popover>
);
return (
<Alert>
{words.map((w) => (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={popover}
>
<span onClick={() => clickCallback(w)}> {w}</span>
</OverlayTrigger>
))}
</Alert>
);
}
UPDATE:
Changed the apiData initialization and the <Popover.Body> component. That hasn't fixed the problem.
const [apiData, setApiData] = useState(null)
<Popover.Body>
{
apiData ?
<div>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</div> :
<div>Loading...</div>
}
</Popover.Body>
The Problem
Here's what I think is happening:
Component renders
Start fetching definition for "luminous".
The definition of "luminous" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "luminous", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("luminous") does nothing.
If you click another word, such as "pity", the popper attempts to show, but setSelectedWord("pity") causes the component to start rerendering.
Component rerenders
Start fetching definition for "pity".
The definition of "pity" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "pity", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("pity") does nothing.
Selecting another word will repeat this process over and over.
To fix this, you need to first make use of the show property to show the popover after rendering it out if it matches the selected word. But what if the word appears multiple times? If you did this for the word "her", it would show the popover in multiple places. So instead of comparing against each word, you'd have to assign each word a unique ID and compare against that.
Fixing the Component
To assign words an ID that won't change between renders, we need to assign them IDs at the top of your component and store them in an array. To make this "simpler", we can abstract that logic into a re-useable function outside of your component:
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading punctuation
.replace(/[.,!?"]+$/, "") // remove trailing punctuation
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
// we can use this to prevent unnecessary rerenders
words.text = text;
// return the array-object
return words;
}
Within the component, we need to setup the state variables to hold the words array. By passing a callback to useState, React will only execute it on the first render and skip calling it on rerenders.
// set up state array of words that have their own UUIDs
// note: we don't want to call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
Now that we have words and _setWords, we can pull out the text value from it:
// extract text from words array for convenience
// probably not needed
const text = words.text;
Next, we can create our own setText callback. This could be simpler, but I wanted to make sure we support React's mutating update syntax (setText(oldValue => newValue)):
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
Next, we need to set up the currently selected word. Once the definition is available, this word's popover will be shown.
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
If you don't want to show a word by default, use:
const [selectedWordObj, setSelectedWordObj] = useState(); // nothing selected by default
To fix the API call, we need to make use of the "use async effect" pattern (there are libraries out there to simplify this):
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
The above code block makes sure to prevent calling the setApiData methods when they aren't needed any more. It also uses a status property to track it's progress so you can render the result properly.
Now to define a popover that shows a loading message:
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
We can mix that loading popover with apiData to get a popover to show the definition. If we're still loading the definition, use the loading one. If we've had an error, show the error. If it completed properly, render out the defintion. To make this easier, we can put this logic in a function outside of your component like so:
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
We call this funtion in the component using:
const selectedWordPopover = getPopover(apiData, loadingPopover);
Finally, we render out the words. Because we are rendering out an array, we need to use a key property that we'll set to each word's Id. We also need to select the word that was clicked - even if there were more than one of the same words, we only want just the clicked one. For that we'll check its Id too. If we click on a particular word, we need to sure that the one we clicked on is selected. We also need to render out the original word with its punctuation. This is all done in this block:
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
Complete Code
Bringing all that together gives:
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading characters
.replace(/[.,!?"]+$/, "") // remove trailing characters
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
words.text = text;
// return the array
return words;
}
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
export default function App() {
// set up state array of words that have their own UUIDs
// note: don't call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
// extract text from words array for convenience
const text = words.text;
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
const selectedWordPopover = getPopover(apiData, loadingPopover);
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
}
Note: You can improve this by caching the results from the API call.

React Window How to Pass in Components?

I am trying to implement react-window but Ia m not sure how to pass in a component that takes in it's own properties
If I have something like this
{
items.map(
(item, index) => {
<MyComponent
key={key}
item={item}
/>;
}
);
}
how do I make a variable list?
The example does not show how to do this
import { VariableSizeList as List } from 'react-window';
// These row heights are arbitrary.
// Yours should be based on the content of the row.
const rowHeights = new Array(1000)
.fill(true)
.map(() => 25 + Math.round(Math.random() * 50));
const getItemSize = index => rowHeights[index];
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Example = () => (
<List
height={150}
itemCount={1000}
itemSize={getItemSize}
width={300}
>
{Row}
</List>
);
The example is indeed confusing. This example shows how you can use react-window with an array of data instead of the generative example that the homepage shows:
class ComponentThatRendersAListOfItems extends PureComponent {
render() {
// Pass items array to the item renderer component as itemData:
return (
<FixedSizeList
itemData={this.props.itemsArray}
{...otherListProps}
>
{ItemRenderer}
</FixedSizeList>
);
}
}
// The item renderer is declared outside of the list-rendering component.
// So it has no way to directly access the items array.
class ItemRenderer extends PureComponent {
render() {
// Access the items array using the "data" prop:
const item = this.props.data[this.props.index];
return (
<div style={this.props.style}>
{item.name}
</div>
);
}
}
While itemsArray is not provided in the code sample, ostensibly you would include the props you need to pass to ItemRenderer in it, such as name as shown here. This would leave your usage looking something like this:
<ComponentThatRendersAListOfItems
itemsArray={[{ name: "Test 1" }, { name: "Test 2" }]}
/>

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