SetState of an Objects of array in React - javascript

So i've been working on this for awhile what i'm trying to do is change a checked value on checkbox click.
My initial state looks like this:
const [todoList, setTodoList] = useState({
foundation: {
steps: [
{ key: "1", title: "setup virtual office", isDone: false },
{ key: "2", title: "set mission and vision", isDone: false },
{ key: "3", title: "select business name", isDone: false },
{ key: "4", title: "buy domain", isDone: false },
],
},
discovery: {
steps: [
{ key: "1", title: "create roadmap", isDone: false },
{ key: "2", title: "competitor analysis", isDone: false },
],
}
});
and my map and onClick function (updateCheckFoundation works when click the checkbox)
{todoList.foundation.steps.map((item) => {
return (
<div>
<input type="checkbox" defaultChecked={item.isDone}
onClick={(event)=> updateCheckFoundation({
isDone:event.target.checked,
key:item.key
})}/>
<span>{item.title}</span>
</div>
);
})}
so how can ı update todoList use setState?
my code (updateCheckFoundation func.) like this and is not working :( :
const updateCheckFoundation = ({isDone, key}) => {
const updateTodoList = todoList.foundation.steps.map((todo)=> {
if(todo.key === key){
return {
...todo,
isDone
};
}
return todo;
});
setTodoList(updateTodoList);
}

Issue
Your updateCheckFoundation callback isn't maintaining the state invariant, and is in fact, dropping all but the foundation.steps array of state.
const updateCheckFoundation = ({isDone, key}) => {
const updateTodoList = todoList.foundation.steps.map((todo)=> {
if(todo.key === key){
return {
...todo,
isDone
};
}
return todo;
});
setTodoList(updateTodoList); // <-- only the state.foundation.steps array!!
}
Solution
In function components, when using the useState state updater functions you need to handle merging state (the root state), and nested state, yourself, manually.
const updateCheckFoundation = ({ isDone, key }) => {
setTodoList(state => ({
...state, // <-- shallow copy state object
foundation: {
...state.foundation, // <-- shallow copy
steps: state.foundation.steps.map(todo => todo.key === key
? { ...todo, isDone }
: todo)
},
}));
}

Related

Filtering out a calculated value in in an object

I tried to only show the relevant objects only and strip as much as possible non-relevant things.
allComponentsFiltered returns a combination of 3 things,
search input
component_group_id
selectedComponent .status
// Desired objective:
I've created a tab with a value of inactive, which does not match a component status status.inactive
The idea is to return in the filter function the components that do not have status.active as true.
const state = {
componentStatusTabs: [
{ name: "All", value: "all", icon: "mdi-all-inclusive" },
{ name: "Starred", value: "starred", icon: "mdi-star" },
{ name: "Modular", value: "modular", icon: "mdi-view-module" },
{ name: "Active", value: "active", icon: "mdi-lightbulb-on" },
{ name: "Inactive", value: "inactive", icon: "mdi-lightbulb-off" }
],
};
// How the component statuses object looks.
selectedComponent = {
component_group_id: 81,
status: {
active: true,
modular: false,
starred: false,
}
}
// Returns the name of the tab name selected within the form field editor
activeComponentEditFormFieldsStatusTabName: state => {
return state.componentStatusTabs[state.activeStatusTab].value;
},
// Returns components that either belong to the selected group, matches the search string or has the corresponding status.
allComponentsFiltered: (state, getters, rootState) => {
if (!getters.hasSelectedSomeGroups) return [];
const search = rootState.application.search.toLowerCase();
return state.allComponents.filter(component => {
return (
(search === "" || component.config.general_config.title.toLowerCase().match(search)) &&
(getters.activeComponentEditFormFieldsStatusTabName === "all" || component.status[getters.activeComponentEditFormFieldsStatusTabName]) &&
state.selectedComponentGroups.some(group => group.id === component.component_group_id)
);
});
}
Answering to my self after I found the solution:
allComponentsFiltered: (state, getters, rootState) => {
if (!getters.hasSelectedSomeGroups) return [];
const search = rootState.application.search.toLowerCase();
return state.allComponents.filter(component => {
return (
(search === "" || component.config.general_config.title.toLowerCase().match(search)) &&
(getters.activeComponentEditFormFieldsStatusTabName === "all" ||
(getters.activeComponentEditFormFieldsStatusTabName === "inactive" && !component.status.active) ||
component.status[getters.activeComponentEditFormFieldsStatusTabName]) &&
state.selectedComponentGroups.some(group => group.id === component.component_group_id)
);
});
},
I added
|| (getters.activeComponentEditFormFieldsStatusTabName === "inactive" && !component.status.active)

Update array of object state in react js

I have the following code snippet from my component where I generate Input field according to the objects in the state.
I can successfully generate the input fields but have been getting error message:
TypeError: Cannot read property 'map' of undefined
Pointing to the method arrayObjToArrary in Utils.js whenever I type in the input field.
How can I update the value of here ??
Main.js
import Input from "../UI/Input";
import {arrayObjToArrary} from "../../utility/Utils.js";
const [profiles, setProfiles] = useState({
controls: [
{
network: {
elementType: "input",
elementConfig: {
type: "text",
label: "Network",
},
value: "Twitter",
},
},
{
username: {
elementType: "input",
elementConfig: {
type: "text",
label: "Username",
},
value: "#john",
},
},
{
url: {
elementType: "input",
elementConfig: {
type: "url",
label: "URL",
},
value: "https://example.xyz",
},
},
],
});
const profilesControls = arrayObjToArrary(profiles.controls);
const arrayInputHandler = (event, index, identifier) => {
const list = [...profiles.controls];
list[index][identifier] = event.target.value;
setProfiles(list);
};
let profileField = profilesControls.map((formElement) => (
<Input
label={formElement.config.elementConfig.label}
key={formElement.index}
type={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
changed={(event) => arrayInputHandler(event, formElement.index, formElement.id)}
/>
));
Utils.js
export const arrayObjToArrary = (controls) => {
const formElementsArray = controls.map((item,index) =>({
id: Object.keys(item)[0],
index:index,
config: item[Object.keys(item)[0]],
}))
return formElementsArray;
}
You can try this
const arrayInputHandler = (event, index, identifier) => {
const list = [...profiles.controls];
list[index][identifier].value = event.target.value;
setProfiles({ controls: list });
};
check here codesandbox
The issue in how you update your profiles object in arrayInputHandler. When you pass in the list to setProfiles, it changes its structure from an object to array.
Also you must not mutate the original state values. The correct way to update is as below
const arrayInputHandler = (event, index, identifier) => {
const value = event.target.value;
setProfiles(prev => ({
...prev,
controls: profiles.controls.map((controls, i) => {
if(i === index) {
return {
...controls, [identifier]: {
...controls[identifier],
value
}
}
}
return controls
});
}));
};
P.S. You can always solve your problem in a simplistic manner like
const arrayInputHandler = (event, index, identifier) => {
const list = [...profiles.controls];
list[index][identifier] = event.target.value;
setProfiles({profile:list});
};
However its not a correct approach and should be avoided as react relies a lot on immutability for a lot of its re-rendering and other optimizations

In React/Redux reducer, how can I update a string in an nested array in an immutable way?

My store looks like this,
{
name: "john",
foo: {},
arr: [
{
id:101,
desc:'comment'
},
{
id:101,
desc:'comment2'
}
]
}
My textarea looks like this
<textarea
id={arr.id} //"101"
name={`tesc:`}
value={this.props.store.desc}
onChange={this.props.onChng}
/>
My action is
export const onChng = (desc) => ({
type: Constants.SET_DESC,
payload: {
desc
}
});
My reducer
case Constants.SET_DESC:
return update(state, {
store: {
streams: {
desc: { $set: action.payload.desc }
}
}
});
It works only if arry is an object, I had to make changes to the stream to an array and I am confused how I can update to an array, also how does get the right value from the store.
The following example taken from the redux documentation might help you in the use case how to update items in an array. For more on this you can read on here http://redux.js.org/docs/recipes/StructuringReducers.html
state structure is something like this
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
and reducer code is like below
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
function appReducer(state = initialState, action) {
switch(action.type) {
case 'EDIT_TODO' : {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, {text : action.text});
});
return updateObject(state, {todos : newTodos});
}
default : return state;
}
}
If you have to update an element in a array within your store you have to copy the array and clone the matching element to apply your changes.
So in the first step your action should contain either the already cloned (and changed) object or the id of the object and the properties to change.
Here is a rough example:
export class MyActions {
static readonly UPDATE_ITEM = 'My.Action.UPDATE_ITEM';
static updateItem(id: string, changedValues: any) {
return { type: MyActions.UPDATE_ITEM, payload: { id, changedValues } };
}
}
export const myReducer: Reducer<IAppState> = (state: IAppState = initialState, action: AnyAction): IAppState => {
switch (action.type) {
case MyActions.UPDATE_ITEM:
return { ...state, items: merge(state.items, action.payload) };
default:
return state;
}
}
const merge = (array, change) => {
// check if an item with the id already exists
const index = array.findIndex(item => item.id === change.id);
// copy the source array
array = [...array];
if(index >= 0) {
// clone and change the existing item
const existingItem = array[index];
array[index] = { ...existingItem, ...change.changedValues };
} else {
// add a new item to the array
array.push = { id: change.id, ...change.changedValues };
}
return array;
}
To update an array, I would use immutability helper and do something like this - to your reducer
let store = {"state" : {
"data": [{
"subset": [{
"id": 1
}, {
"id": 2
}]
}, {
"subset": [{
"id": 10
}, {
"id": 11
}, {
"id": 12
}]
}]
}}
case Constants.SET_DESC:
return update(store, {
"state" : {
"data": {
[action.indexToUpdate]: {
"subset": {
$set: action.payload.desc
}
}
}
}
})
});

React.DOM: Unhandled Rejection (TypeError): Cannot read property 'map' of undefined

Being a newbie to the React community...I'm blocked (for hours now) and unable to trace a solution to fix the error posted above:
Am I missing the right parameters to how the data object is fetched in through the app?
This is my ajax data response
The bug is living on props.personList.map inside of const ListContainer
For context here's the code on the entire file:
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
function getPersonList() {
const api = 'apistring';
return axios.get(api).then(res => {
console.log(res);
}).catch(err => console.log(err));
}
let getLastName = (fullName) => {
return fullName.match(/\w+/g)[1];
};
const getFirstName = (fullName) => {
return fullName.match(/\w+/g)[0];
};
//Remove any people that do not have the name we are searching for
let filterByName = (searchForName, personList) => {
return personList.filter((person) => {
return person.name === searchForName;
});
};
//VIEW (React)
const Search = ({ onChange }) => React.DOM.input({
type: 'input',
onChange
});
const Thumbnail = ({src}) => React.DOM.img({
className: 'image',
src
});
//CODE BREAKS HERE
const ListRow = (props) => React.DOM.tr({ key: props.person.name }, [
React.DOM.td({ key: 'headshot' }, React.createElement(Thumbnail, { src: props.person.url })),
React.DOM.td({ key: 'firstName' }, null, getFirstName(props.person.name)),
React.DOM.td({ key: 'lastName' }, null, getLastName(props.person.name)),
]);
const ListContainer = (props) => React.DOM.table({ className: 'list-container' }, [
React.DOM.thead({ key: 'firstName' }, React.DOM.tr({}, [
React.DOM.th({ key: 'lastName' }, null, 'headshot'),
React.DOM.th({ key: 'id' }, null, 'First Name'),
React.DOM.th({ key: 'last-h' }, null, 'Last Name')
])),
React.DOM.tbody({ key: 'tbody' }, props.personList.map((person, i) =>
React.createElement(ListRow, { key: `person-${i}`, person })))
]);
const App = React.createClass({
getInitialState() {
return {
personList: [],
visiblePersonList: []
};
},
componentDidMount() {
getPersonList().then((data) =>
this.setState({
data,
visiblePersonList: data
}));
},
_shuffleList() {
this.setState({
visiblePersonList: shuffleList(this.state.personList)
});
},
_sortByFirst() {
this.setState({
visiblePersonList: sortByFirstName(this.state.personList)
});
},
_sortByLast() {
this.setState({
visiblePersonList: sortByLastName(this.state.personList)
});
},
_onSearch(e) {
this.setState({
visiblePersonList: filterByName(e.target.value, this.state.personList)
});
},
render() {
const { visiblePersonList } = this.state;
return React.DOM.div({ className: 'app-container' }, [
React.createElement(Search, { key: 'search', onChange: this._onSearch }),
React.DOM.button({ key: 'shuffle', onClick: this._shuffleList }, null, 'Shuffle'),
React.DOM.button({ key: 'sort-first', onClick: this._sortByFirst }, null, 'Sort (First Name)'),
React.DOM.button({ key: 'sort-last', onClick: this._sortByLast }, null, 'Sort (Last Name)'),
React.createElement(ListContainer, { key: 'list', personList: visiblePersonList })
]);
}
});
ReactDOM.render(<App />, document.getElementById('app'));
Your callback with the console.log accesses the value and then discards it.
function getPersonList() {
const api = 'apistring';
return axios.get(api).then(res => {
console.log(res);
}).catch(err => console.log(err));
}
should be
function getPersonList() {
const api = 'apistring';
return axios.get(api).then(res => {
console.log(res);
return res.data.items;
}).catch(err => console.log(err));
}
When you use .then you are inserting a link into your promise chain. The value returned by .then becomes the value passed to the next handler. Because you are not returning any value, your
getPersonList().then((data) => {
// ...
});
callback will get data as undefined.
Another thing to note, though don't cause this specific error, is that your screenshot shows objects with .firstName and .lastName but all of the code in this file uses .name which does not exist.

Keeping data object after filtering

Small Background intro
I´m currently working the User Administration page of my project and running into a small problem here. I have a table which contains some material-ui`s Usercard. For Each user that uses my System exist´s one card. The card´s are generated from data that comes from my database and then written into a redux store.
The Admin can do several interactions with the database that changes some Userdata. To provide an easy way to find a specific user a <TextField /> was implemented that filter´s the table of Usercards.
All of the things mentioned here works!
The Problem
As mentioned in the Intro the data are stored in a redux store. When I filter the data, an action is dispatched
export const FILTER_ALL_USER_BY_NAME = "FILTER_ALL_USER_BY_NAME"
export const FILTER_ALL_USER_BY_DEPARTMENT = "FILTER_ALL_USER_BY_DEPARTMENT"
export default function filterAllUser(filter, filterOption){
return (dispatch, getState) => {
if(filterOption === 'name'){
return dispatch (filterUserByName(filter))
}else{
return dispatch (filteUserByDepartment(filter))
}
}
}
function filterUserByName(filter){
return {
type: FILTER_ALL_USER_BY_NAME,
filter: filter
}
}
function filteUserByDepartment(filter){
return {
type: FILTER_ALL_USER_BY_DEPARTMENT,
filter: filter
}
}
The Reducer
Even if the reducers works, it is the main reason for my problem.
Why?
It is because, when I filter the data I was not able to really filter the state, rather then return a new state object which leads me to the problem that the allUserData and filteredUserData get out of sync after the userdata are changed.
Let me explain this in code
function allUser(state = {allUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return Object.assign({}, state, {
isFetching: true
});
case 'RECEIVE_ALL_USER':
return Object.assign({}, state, {
isFetching: false,
allUserData: action.items
});
case 'FILTER_ALL_USER_BY_NAME':
return Object.assign({}, state, {
filteredUserData: state.allUserData.filter(user => user.userNameLast.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0)
});
case 'FILTER_ALL_USER_BY_DEPARTMENT':
return Object.assign({}, state, {
filteredUserData: state.allUserData.filter(user => user.departmentName.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0)
});
default:
return state
}
}
But when I´m trying to filter the original state and the user removes the filter, the data that didn´t matched the filter are gone.
case 'FILTER_ALL_USERS': return allUsers.filter(user => user.userNameLast.toLowerCase().indexOf(action.filter.toLowerCase()) >= 0);
How can I filter the state, but keep the data ?
As requested, I have put together some code for you. It would be something like below.
As a side note, I would pass the filter as { field: 'userLastName', text: your filter text} as the filter criteria. Or to make it even more scalable, you can pass a filter-handler instead of text above. That way you can have any type of filter, and not just text filters.
export default function allUser(state = {allUserData: [], filters: {}, filteredUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return {
...state,
isFetching: true
};
case 'RECEIVE_ALL_USER':
return {
...state,
isFetching: false,
allUserData: action.items,
filteredUserData: filterData(action.items, state.filters),
};
case 'FILTER_ALL_USER_BY_NAME':
{
const updatedFilters = {
...state.filters,
userNameLast: action.filter
}
return {
...state,
filteredUserData: filterData(state.allUserData, updatedFilters),
filters: updatedFilters
};
}
case 'FILTER_ALL_USER_BY_DEPARTMENT':
{
const updatedFilters = {
...state.filters,
departmentName: action.filter
}
return {
...state,
filteredUserData: filterData(state.allUserData, updatedFilters),
filters: updatedFilters
};
}
default:
return state
}
}
const filterData = (users, filters) => {
return users.filter(filterFn(filters));
};
const filterFn = filters => item => Object.keys(filters).reduce((res, filter) => {
return res && item[filter].toLowerCase().indexOf(filters[filter].toLowerCase()) >= 0;
}, true);
Unit tests
import usersReducer from './users';
describe('usersReducer', () => {
describe('RECEIVE_ALL_USER', () => {
const RECEIVE_ALL_USER = 'RECEIVE_ALL_USER';
it('should replace users in state', () => {
const initialState = { isFetching: false, allUserData: [{ name: 'A' }], filters: {}};
const newUsers = [{ name: 'B' }];
const newState = { ...initialState, allUserData: newUsers, filteredUserData: newUsers};
expect(usersReducer(initialState, { type: RECEIVE_ALL_USER, items: newUsers })).toEqual(newState);
})
})
describe('FILTER_ALL_USER_BY_NAME', () => {
let FILTER_ALL_USER_BY_NAME = 'FILTER_ALL_USER_BY_NAME';
it('should filter users by name', () => {
const initialState = { isFetching: false, allUserData: [{ userNameLast: 'Doe' }, { userNameLast: 'Smith' }], filters: {}};
const filterText = 'd';
const finalState = { isFetching: false,
allUserData: [{ userNameLast: 'Doe' }, { userNameLast: 'Smith' }],
filters: { userNameLast: filterText },
filteredUserData: [{ userNameLast: 'Doe' }]
};
expect(usersReducer(initialState, { type: FILTER_ALL_USER_BY_NAME, filter: filterText})).toEqual(finalState);
})
})
describe('FILTER_ALL_USER_BY_DEPARTMENT', () => {
let FILTER_ALL_USER_BY_DEPARTMENT = 'FILTER_ALL_USER_BY_DEPARTMENT';
it('should filter users by department', () => {
const initialState = { isFetching: false, allUserData: [{ departmentName: 'IT' }, { departmentName: 'Human Resources' }], filters: {}};
const filterText = 'it';
const finalState = {
...initialState,
filters: { departmentName: filterText },
filteredUserData: [{ departmentName: 'IT' }]
};
expect(usersReducer(initialState, { type: FILTER_ALL_USER_BY_DEPARTMENT, filter: filterText})).toEqual(finalState);
})
})
});
Best for these cases is to some selector library, example reselect. Instead of editing the original state create selectors for sorting and filter and pass the result to component.
There's also a quite similar example in reselect documentation https://github.com/reactjs/reselect#selectorstodoselectorsjs.

Categories