Update Nested Objects in React Native using setState - javascript

I am trying to change the properties of objects inside of an object and
trying to add new properties to these objects but keeping the old values.
I can't find out how to get the right nested object by index, not id because
the id can be different from the .map index.
This is what I got so far, the Object names are for testing purposes
only and maybe the "updateNestedObject" can be run in the parent?
Thank you in advance and sorry if this is a noob question.
Neval
import React, { useState } from 'react';
import { View, TextInput, Text, StyleSheet, Button, Alert } from 'react-native';
function ObjectScreen() {
const [state, setState] = useState({
id: 1,
name: 'Test Object',
nested: [
{
id: 1,
title: 'Object 1',
},
{
id: 2,
title: 'Object 1',
}
]
});
function editNested({nestedObject, index, setState}) {
const updateNestedObject = () => {
setState(prevState => ({
nested: [
...prevState.nested,
[prevState.nested[index].comment]: 'Test Comment',
},
}));
}
return (
<View>
<Text>{object.title}</Text>
<TextInput
style={styles.input}
name="comment"
onChangeText={updateNestedObject}
/>
</View>
);
}
return (
<>
<Text>{state.name}</Text>
{ state.nested.map((nestedObject, key)=>{
return (
<editNested key={key} index={key} object={object} nestedObject={nestedObject}/>
)
})}
</>
)
}
const styles = StyleSheet.create({
container: {},
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
export default ObjectScreen;

There were few issues:
JSX component name editNested should start with a capital letter.
And editNested component should be on its own function, should not define inside another component which caused your TextInput to lose focus after each render cycle.
The setState call should be changed like below:
const updateNestedObject = (text) => {
setState((prevState) => ({
...prevState,
nested: prevState.nested.map((item) =>
item.id === nestedObject.id ? { ...item, value: text } : item
)
}));
};
Try the code below:
import React, { useState } from "react";
import { View, TextInput, Text, StyleSheet, Button, Alert } from "react-native";
function EditNested({ nestedObject, setState }) {
const updateNestedObject = (text) => {
setState((prevState) => ({
...prevState,
nested: prevState.nested.map((item) =>
item.id === nestedObject.id ? { ...item, value: text } : item
)
}));
};
return (
<View>
<Text>{nestedObject.title}</Text>
<TextInput
style={styles.input}
onChangeText={updateNestedObject}
value={nestedObject.value}
/>
</View>
);
}
function ObjectScreen() {
const [state, setState] = useState({
id: 1,
name: "Test Object",
nested: [
{
id: 1,
title: "Object 1",
value: ""
},
{
id: 2,
title: "Object 1",
value: ""
}
]
});
console.log(state);
return (
<>
<Text>{state.name}</Text>
{state.nested.map((nestedObject, key) => {
return (
<EditNested
key={key}
nestedObject={nestedObject}
setState={setState}
/>
);
})}
</>
);
}
const styles = StyleSheet.create({
container: {},
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10
}
});
export default ObjectScreen;
Working Demo

As per que you can update nested array with below method
const updateNestedObject = (values, item, index) => {
console.log('values', values);
const tempMainObj = state;
const tempArr = state.nested;
tempArr[index].value = values;
const updatedObj = { ...tempMainObj, nested: tempArr };
setState(updatedObj);
};
Full Example
import React, { useState } from 'react';
import { View, TextInput, Text, StyleSheet, Button, Alert } from 'react-native';
function ObjectScreen() {
const [state, setState] = useState({
id: 1,
name: 'Test Object',
nested: [
{
id: 1,
title: 'Object 1',
value: '',
},
{
id: 2,
title: 'Object 1',
value: '',
},
],
});
const updateNestedObject = (values, item, index) => {
console.log('values', values);
const tempMainObj = state;
const tempArr = state.nested;
tempArr[index].value = values;
const updatedObj = { ...tempMainObj, nested: tempArr };
setState(updatedObj);
};
return (
<>
<Text style={{ marginTop: 50 }}>{state.name}</Text>
{state.nested.map((item, index) => {
return (
<>
<Text>{item.title}</Text>
<TextInput
value={item?.value}
style={styles.input}
name="comment"
onChangeText={(values) => updateNestedObject(values, item, index)}
/>
</>
);
})}
</>
);
}
const styles = StyleSheet.create({
container: {
marginTop: 50,
},
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
export default ObjectScreen;
Snack expo: Live Example

Related

React Native Searchable Flatlist using Nested JSON

I am trying to make a searchable flatlist for skills using the below JSON file:
const employeeList = [
{
id: "1",
name: "John",
image: require("../images/John.png"),
skills: [
{ id: 1, name: "Cooking" },
{ id: 2, name: "Climbing" },
],
},
{
id: "2",
name: "Pat",
image: require("../images/Pat.png"),
skills: [
{ id: 1, name: "Cooking" },
{ id: 2, name: "Rowing" },
],
},
];
export default employeeList;
I was successful in making a screen that will display and allow me to search for employee names but I would like to make a searchable flatlist with all the skills while also displaying the employee name who has that skill. I don't need them to be unique. In my code below I have managed to display all employees and their skills however my search feature only filters for the employee name.
// Searching using Search Bar Filter in React Native List View
// https://aboutreact.com/react-native-search-bar-filter-on-listview/
// import React in our code
import React, { useState, useEffect } from "react";
// import all the components we are going to use
import {
SafeAreaView,
Text,
StyleSheet,
View,
FlatList,
TextInput,
Image,
TouchableOpacity,
} from "react-native";
// import employee json
import employeeList from "../json/employee";
const AllListScreen = ({ navigation, route }) => {
const [search, setSearch] = useState("");
const [filteredDataSource, setFilteredDataSource] = useState([]);
const [masterDataSource, setMasterDataSource] = useState([]);
// set employee json as filter source
useEffect(() => {
setFilteredDataSource(employeeList);
setMasterDataSource(employeeList);
// skills show as undefined unless index is specified
console.log(JSON.stringify(employeeList[0].skills));
}, []);
const searchFilterFunction = (text) => {
// Check if searched text is not blank
if (text) {
// Inserted text is not blank
// Filter the masterDataSource
// Update FilteredDataSource
const newData = masterDataSource.filter(function (item) {
const itemData = item.name ? item.name.toUpperCase() : "".toUpperCase();
const textData = text.toUpperCase();
return itemData.indexOf(textData) > -1;
});
setFilteredDataSource(newData);
setSearch(text);
} else {
// Inserted text is blank
// Update FilteredDataSource with masterDataSource
setFilteredDataSource(masterDataSource);
setSearch(text);
}
};
const ItemView = ({ item, index }) => {
return (
// Flat List Item
<View>
// use map to display all skills under employee
{item.skills.map((v, i) => (
<>
<TouchableOpacity
onPress={() => console.log(v.name)}
style={styles.itemStyle}
key={item.id}
>
<Image
source={{ uri: "https://source.unsplash.com/random" }}
style={{ height: 50, width: 50 }}
/>
<View style={styles.textPortion}>
<Text>{item.name}</Text>
<Text>{v.name.toUpperCase()}</Text>
</View>
</TouchableOpacity>
<ItemSeparatorView />
</>
))}
</View>
);
};
const ItemSeparatorView = () => {
return (
// Flat List Item Separator
<View
style={{
height: 0.5,
width: "100%",
backgroundColor: "#C8C8C8",
}}
/>
);
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<TextInput
style={styles.textInputStyle}
onChangeText={(text) => searchFilterFunction(text)}
value={search}
underlineColorAndroid="transparent"
placeholder="Search Here"
/>
<FlatList
data={filteredDataSource}
keyExtractor={(item, index) => index.toString()}
renderItem={ItemView}
/>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: "#FFFFFF",
},
itemStyle: {
flex: 1,
padding: 8,
flexDirection: "row",
},
textInputStyle: {
height: 50,
borderWidth: 1,
paddingLeft: 20,
margin: 6,
borderColor: "#009688",
backgroundColor: "#FFFFFF",
borderRadius: 5,
},
textPortion: {
flexWrap: "wrap",
flexShrink: 1,
marginLeft: 6,
},
});
export default AllListScreen;
Here is an image of how it is displayed but as stated the search only works on the employee name while I want it to work on the skill:
Any help is much appreciated. Thanks.
I managed to get this working by restructuring the JSON in an array:
skillArray = [];
for (var key in employeeList) {
if (employeeList.hasOwnProperty(key)) {
for (item in employeeList[key].skills) {
skillArray.push({
name: employeeList[key].name,
skill: employeeList[key].skills[item].name,
});
}
}
}
Then I changed my search filter to target the skill instead of the name:
// set employee json as filter source
useEffect(() => {
setFilteredDataSource(skillArray);
setMasterDataSource(skillArray);
// skills show as undefined unless index is specified
console.log(JSON.stringify(employeeList[0].skills));
}, []);
const searchFilterFunction = (text) => {
// Check if searched text is not blank
if (text) {
// Inserted text is not blank
// Filter the masterDataSource
// Update FilteredDataSource
const newData = masterDataSource.filter(function (item) {
const itemData = item.skill ? item.skill.toUpperCase() : "".toUpperCase();
const textData = text.toUpperCase();
return itemData.indexOf(textData) > -1;
});
setFilteredDataSource(newData);
setSearch(text);
} else {
// Inserted text is blank
// Update FilteredDataSource with masterDataSource
setFilteredDataSource(masterDataSource);
setSearch(text);
}
};
And my updated ItemView:
const ItemView = ({ item }) => {
return (
// Flat List Item
<View style={styles.itemStyle}>
<Image
source={{ uri: "https://source.unsplash.com/random" }}
style={{ height: 50, width: 50 }}
/>
<Text style={styles.textPortion}>
{item.name.toUpperCase()}
{"\n"}
{item.skill.toUpperCase()}
</Text>
</View>
);
};
I'm not sure if this is the optimal approach but hopefully it helps someone.

react async select add new options in select not getting updated

how to add option to react-select/async component explicitly on click of add option , I'm not able to update options , but state is getting updated !!
is there any other way to achive this ?
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import AsyncSelect from "react-select/async";
const MyComponent = () => {
const optionsData = [
{ value: "Spring", label: "Spring" },
{ value: "Summer", label: "Summer" },
{ value: "Autumn", label: "Autumn" },
{ value: "Winter", label: "Winter" }
];
const [options, setOptions] = useState(optionsData);
const loadOptions = (inputValue) => {
console.log(inputValue, "pppp");
return new Promise((resolve, reject) => {
// using setTimeout to emulate a call to server
setTimeout(() => {
resolve(filter(inputValue));
}, 2000);
});
};
const filter = (inputValue) =>
options.filter((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase())
);
const handleAddOption = () => {
const newOptions = [
{ value: "Apple", label: "Apple" },
{ value: "Ball", label: "Ball" }
];
setOptions((prevState) => [...prevState, ...newOptions]);
};
return (
<div style={{ display: "flex", justifyContent: "space-around" }}>
<div style={{ width: "400px" }}>
<AsyncSelect defaultOptions cacheOptions loadOptions={loadOptions} />
</div>
<button onClick={handleAddOption}>ADD OPTIONS</button>
</div>
);
};
ReactDOM.render(<MyComponent />, document.getElementById("app"));
CODESANDBOX LINK HERE
You should use options state as the value of defaultOptions prop of AsyncSelect component. From the docs about defaultOptions:
Providing an option array to this prop will populate the initial set of options used when opening the select, at which point the remote load only occurs when filtering the options (typing in the control)
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
import AsyncSelect from "react-select/async";
const MyComponent = () => {
const optionsData = [
{ value: "Spring", label: "Spring" },
{ value: "Summer", label: "Summer" },
{ value: "Autumn", label: "Autumn" },
{ value: "Winter", label: "Winter" }
];
const [options, setOptions] = useState(optionsData);
const loadOptions = (inputValue) => {
return new Promise((resolve, reject) => {
// using setTimeout to emulate a call to server
setTimeout(() => {
resolve(filter(inputValue));
}, 2000);
});
};
const filter = (inputValue) =>
options.filter((option) =>
option.label.toLowerCase().includes(inputValue.toLowerCase())
);
const handleAddOption = () => {
const newOptions = [
{ value: "Apple", label: "Apple" },
{ value: "Ball", label: "Ball" }
];
setOptions((prevState) => [...prevState, ...newOptions]);
};
return (
<div style={{ display: "flex", justifyContent: "space-around" }}>
<div style={{ width: "400px" }}>
<AsyncSelect
defaultOptions={options}
cacheOptions
loadOptions={loadOptions}
/>
</div>
<button onClick={handleAddOption}>ADD OPTIONS</button>
</div>
);
};
ReactDOM.render(<MyComponent />, document.getElementById("app"));
CodeSandbox

ReactNative deep cloning state for Flatlist

My FlatList renderItem was re-rendering every item when one of them was changed.
After doing some debugging i deepcloned the state variable which holds the items (+ React.memo), it's working fine now but not sure if it's the optimal solution.
Snack: https://snack.expo.io/-419PhiUl
App.js
import * as React from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import Constants from 'expo-constants';
import _ from 'lodash';
import Item from './components/Item';
const keyExtractor = item => item.id.toString();
export default function App() {
const [data, setData] = React.useState([
{id: 1, title: 'Post 1', liked: false, user: {name: 'A'}},
{id: 2, title: 'Post 2', liked: false, user: {name: 'B'}},
{id: 3, title: 'Post 3', liked: false, user: {name: 'C'}},
]);
/**
* Like / Unlike the item.
*/
const like = React.useCallback((id) => {
setData(state => {
let clonedState = [...state];
let index = clonedState.findIndex(item => item.id === id);
clonedState[index].liked = ! clonedState[index].liked;
return clonedState;
});
}, []);
/**
* Render items.
*/
const renderItem = React.useCallback(({item}) => (
<Item item={item} onLike={like} />
), []);
const deepClonedData = React.useMemo(() => _.cloneDeep(data), [data]);
return (
<View style={styles.container}>
<FlatList
data={deepClonedData}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
}
});
Item.js
import React from 'react';
import {
Text, TouchableOpacity, StyleSheet
} from 'react-native';
function Item({item, onLike}) {
const _onLike = React.useCallback(() => {
onLike(item.id);
}, []);
console.log('rendering', item.title);
return (
<TouchableOpacity onPress={_onLike} style={styles.item}>
<Text>{item.title} : {item.liked ? 'liked' : 'not liked'}</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
item: {
marginVertical: 10,
backgroundColor: 'white',
padding: 15,
borderWidth: 1
}
});
const areEqual = (prevProps, nextProps) => {
return prevProps.item.liked === nextProps.item.liked;
}
export default React.memo(Item, areEqual);

How to add and edit list items in a dual list in reactjs?

I am pretty new to React js and trying different ways to make a to-do list to understand it further. I have a parent component that renders two child components. I figured out how to transfer the items between the two lists. How do I add items to the 2 lists separately from the UI? I am not able to figure that out. I need two input textboxes for each list and also should be able to edit the list items. Can anybody please help me?
import React,{useState,useEffect} from 'react'
import { Completed } from './Completed'
import { Pending } from './Pending'
export const Items = () => {
const [items,setItems]=useState([
{
id: 1,
title:'Workout',
status:'Pending'
},
{
id: 2,
title:'Read Books',
status:'Pending'
},
{
id: 3,
title:'Cook Pizza',
status:'Pending'
},
{
id: 4,
title:'Pay Bills',
status:'Completed'
},
{
id: 5,
title:' Watch Big Short',
status:'Completed'
},
{
id: 6,
title:' Make nutrition Plan',
status:'Pending'
}
])
const updateStatus=(id,newStatus)=>{
let allItems=items;
allItems=allItems.map(item=>{
if(item.id===id){
console.log('in here')
item.status=newStatus;
}
return item
})
setItems(allItems)
}
return (
<div class="items">
<Pending items={items} setItems={setItems} updateStatus={updateStatus}/>
<Completed items={items} setItems={setItems} updateStatus={updateStatus}/>
</div>
)
}
import React from 'react'
export const Completed = ({items,setItems,updateStatus}) => {
return (
<div className="completed">
<h1>RIGHT</h1>
{
items && items.map(item=>{
if(item && item.status==='Completed')
return <><p className="item" key={item.id}>{item.title} <button className="mark_pending" key={item.id} onClick={()=>{updateStatus(item.id,'Pending')}}> Move Left</button></p></>
})
}
</div>
)
}
import React from 'react'
export const Pending = ({items,setItems,updateStatus}) => {
return (
<div className="pending">
<h1>LEFT</h1>
{
items && items.map(item=>{
if(item && item.status==='Pending')
return <><p className="item" key={item.id}>{item.title} <button className="mark_complete" key={item.id} onClick={()=>{updateStatus(item.id,'Completed')}}>Move Right</button></p></>
})
}
</div>
)
}
What do you mean by "separately from the UI" ?
import React, { useState } from "react";
const initialStatus = "Pending";
const initialData = [
{
id: 1,
title: "Workout",
status: "Pending",
},
{
id: 2,
title: "Read Books",
status: "Pending",
},
{
id: 3,
title: "Cook Pizza",
status: "Pending",
},
{
id: 4,
title: "Pay Bills",
status: "Completed",
},
{
id: 5,
title: " Watch Big Short",
status: "Completed",
},
{
id: 6,
title: " Make nutrition Plan",
status: "Pending",
},
];
const Box = ({ id, title, status, setItems, items }) => {
return (
<button
onClick={() => {
const newItems = [...items];
const index = items.findIndex((v) => v.id == id);
newItems[index].status =
newItems[index].status == initialStatus ? "Completed" : initialStatus;
setItems(newItems);
}}
>
{title}
</button>
);
};
export const Items = () => {
const [items, setItems] = useState(initialData);
return (
<div style={{ display: "flex" }}>
<div style={{ display: "flex", flexDirection: "column" }}>
<h1>LEFT</h1>
{items
.filter((v) => v.status === initialStatus)
.map((props) => (
<Box {...props} key={props.id} setItems={setItems} items={items} />
))}
</div>
<div style={{ display: "flex", flexDirection: "column" }}>
<h1>Right</h1>
{items
.filter((v) => v.status !== initialStatus)
.map((props) => (
<Box {...props} key={props.id} setItems={setItems} items={items} />
))}
</div>
</div>
);
};
export default Items;

Why adding extra state helps to update other state?

Here is the full code:
import * as React from 'react';
import { View, ScrollView, StyleSheet } from 'react-native';
import {
Appbar,
Searchbar,
List,
BottomNavigation,
Text,
Button,
} from 'react-native-paper';
const AccordionCollection = ({ data }) => {
var bookLists = data.map(function (item) {
var items = [];
for (let i = 0; i < item.total; i++) {
items.push(
<Button mode="contained" style={{ margin: 10 }}>
{i}
</Button>
);
}
return (
<List.Accordion
title={item.title}
left={(props) => <List.Icon {...props} icon="alpha-g-circle" />}>
<View
style={{
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
backgroundColor: 'white',
}}>
{items}
</View>
</List.Accordion>
);
});
return bookLists;
};
const MusicRoute = () => {
const DATA = [
{
key: 1,
title: 'Zain dishes',
total: 21,
},
{
key: 2,
title: 'Sides',
total: 32,
},
{
key: 3,
title: 'Drinks',
total: 53,
},
{
key: 4,
title: 'Aesserts',
total: 14,
},
];
const [data, setData] = React.useState(DATA);
const [searchQuery, setSearchQuery] = React.useState('');
const [sortAZ, setSortAZ] = React.useState(false);
const onChangeSearch = (query) => {
setSearchQuery(query);
const newData = DATA.filter((item) => {
return item.title.toLowerCase().includes(query.toLowerCase());
});
setData(newData);
};
const goSortAZ = () => {
setSortAZ(true);
setData(
data.sort((a, b) => (a.title > b.title ? 1 : b.title > a.title ? -1 : 0))
);
};
const goUnSort = () => {
setSortAZ(false);
setData(DATA);
};
return (
<View>
<Appbar.Header style={styles.appBar}>
<Appbar.BackAction onPress={() => null} />
<Searchbar
placeholder="Search"
onChangeText={onChangeSearch}
value={searchQuery}
style={styles.searchBar}
/>
<Appbar.Action
icon="sort-alphabetical-ascending"
onPress={() => goSortAZ()}
/>
<Appbar.Action icon="library-shelves" onPress={() => goUnSort()} />
</Appbar.Header>
<ScrollView>
<List.Section title="Accordions">
<AccordionCollection data={data} />
</List.Section>
</ScrollView>
</View>
);
};
const AlbumsRoute = () => <Text>Albums</Text>;
const MyComponent = () => {
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{ key: 'music', title: 'Music', icon: 'queue-music' },
{ key: 'albums', title: 'Albums', icon: 'album' },
]);
const renderScene = BottomNavigation.SceneMap({
music: MusicRoute,
albums: AlbumsRoute,
});
return (
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
/>
);
};
const styles = StyleSheet.create({
appBar: {
justifyContent: 'space-between',
},
searchBar: {
width: '60%',
shadowOpacity: 0,
borderRadius: 10,
backgroundColor: '#e4e3e3',
},
});
export default MyComponent;
Expo Snack Link
There are two weird mechanisms.
First
If I remove sortAZ(true) in goSortAZ() and sortAZ(false) in goUnSort(), the state data stops updating after you press on (1) sort and (2) unsort buttons more than three times.
Second
If I remove DATA array outside the component, sort and unsort buttons does not work/update.
If I do not remove these two, I can sort and unsort the list.
I feel that the code is messy although it achieves the function.
My questions is:
Why adding extra state (sortAZ) helps to update other state (data)?
Just totally remove sortAZ variable (no need to use it unless you somehow want to have a loading status, but since you are not making http requests, that's not necessary) and replace goSortAZ with the following:
Remember to clone the original array in order to create a new copy and then sort that copy.
This is working fine.
const goSortAZ = () => {
setData(
[...data].sort((a, b) => (a.title > b.title ? 1 : b.title > a.title ? -1 : 0))
);
};
i would suggest using the same technique for the unSort method too.

Categories

Resources