React not re-rendering after array state update - javascript

I have a checkbox list UI that is rendered from an array. After I update the array the checkbox list state does not update.
I moved the code where the list is mapped but it does not change the results. The DOM re-render does not happen, see gif below.
I have been looking arround and I see that this issue is already reported however the solution about moving the list.map code out of the function it did not work for me.
Could you suggest me a solution?
What is the source of this problem?
import React,{ useState } from "react"
import
{
Box,
DropButton,
Grid,
Text,
Calendar,
RangeInput,
CheckBox
} from "grommet"
const CalButton = ( { label,onSelect } ) =>
{
return (
<DropButton
label={ label }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium" background="ligth-3" elevation="small">
<Calendar range size="medium" onSelect={ onSelect } />
</Box>
} />
)
}
const RangeButton = ( { label,value,onChange,min,max,step,unit,header } ) =>
{
return (
<DropButton
label={ value === null ? label : value + ' ' + unit }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium"
background="ligth-3"
elevation="small"
align="center"
>
<Text size="small">{ header }</Text>
<RangeInput
value={ value }
min={ min } max={ max }
onChange={ onChange }
step={ step }
/>
<Text weight="bold">{ value }</Text>
<Text weight="normal">{ unit }</Text>
</Box>
} />
)
}
const FeaturesButton = ( { label,features,onChange } ) =>
{
const FeaturesList = ( { features,onChange } ) => (
<>
{ features.map( ( item,idx ) => (
<CheckBox
key={ item.name }
label={ item.name }
checked={ item.checked }
onChange={ e => onChange( e,idx ) } />)
)
}
</>
)
return (
<DropButton
label={ label }
dropAlign={ { top: 'bottom',left: 'left' } }
margin="small"
dropContent={
<Box pad="medium"
background="ligth-3"
elevation="small"
align="start"
direction="column"
gap="small"
>
<FeaturesList
features={features}
onChange={onChange} />
</Box>
} />
)
}
const destApp = () =>
{
const [ windStrength,setWindStrength ] = useState( null )
const [ windFrequency,setWindFrequency ] = useState( null )
const [ cost,setCost ] = useState( null )
const date = new Date()
const [ month,setMonth ] = useState( date.getMonth() )
const [ listFeatures,setListFeatures ] = useState( [
{
name: 'Butter flat water',
checked: false,
},
{
name: 'Moderately flat water',
checked: false,
},
{
name: '1-2m Waves',
checked: false,
},
{
name: '2m+ Waves',
checked: false,
},
{
name: 'Beginer Friendly',
checked: false,
},
{
name: 'Kite-in-kite-out',
checked: false,
},
{
name: 'Nightlife',
checked: false,
}
] )
const months = [ 'January','February','March','April','May','June','July','August','September','October','November','December' ];
const updateFeaturesList = ( e,idx ) =>
{
listFeatures[ idx ].checked = e.target.checked
const newFeatures = listFeatures
setListFeatures( newFeatures )
console.log( "Updated features list",newFeatures,e.target.checked )
}
return (
<Grid rows={ [ "xsmall","fill" ] }
areas={ [ [ "filterBar" ],[ "results" ] ] }
gap="xxsmall">
<Box gridArea="filterBar"
direction="row-responsive"
gap="xsmall"
pad="xsmall"
justify="center" >
<CalButton label={ months[ month ].toLowerCase() } onSelect={ ( data ) => console.log( data ) } />
<RangeButton
label="wind strength"
header="What's your wind preference?"
min="15"
max="45"
unit="kts"
value={ windStrength }
step={ 1 }
onChange={ ( e ) =>
{
setWindStrength( e.target.value )
console.log( windStrength )
} } />
<RangeButton
label="wind frequency"
header="How often does your destination need to be windy?"
min="1"
max="7"
unit="days/week"
value={ windFrequency }
step={ 1 }
onChange={ ( e ) =>
{
setWindFrequency( e.target.value )
console.log( windFrequency )
} } />
<RangeButton
label="cost"
header="Average daily cost: 1 lunch, diner and doubble room at a midrange hotel?"
min="10"
max="400"
unit="€"
value={ cost }
step={ 1 }
onChange={ ( e ) =>
{
setCost( e.target.value )
console.log( cost )
} } />
<FeaturesButton
label="important features "
features={ listFeatures }
onChange={ updateFeaturesList }
/>
</Box>
<Box gridArea="results"
margin="">
Results go in here!
</Box>
</Grid>
)
}
export default destApp

The problem is in updateFeaturesList, you are mutating the state directly in this line listFeatures[ idx ].checked = e.target.checked, the state reference stay the same and so react does not know if it should re-render.
What you can do is copy the state, before changing it :
const updateFeaturesList = ( e,idx ) =>
{
const newFeatures = [...listFeatures];
newFeatures[ idx ].checked = e.target.checked
setListFeatures( newFeatures )
console.log( "Updated features list",newFeatures,e.target.checked )
}

You're mutating the original state in your updateFeaturesList function. Use the functional form of setState to update your current feature list:
const updateFeaturesList = (e, idx) => {
const { checked } = e.target;
setListFeatures(features => {
return features.map((feature, index) => {
if (id === index) {
feature = { ...feature, checked };
}
return feature;
});
});
};
Also note that calling console.log("Updated features list", newFeatures,e.target.checked) immediately after setting the state won't show the updated state, since setting state is async.

React state will trigger render only if value changed in state.
Modifying or pushing values to array wont change the array reference, here react state uses array reference to decide whether to trigger render or not.so here array is modifying but reference is not changing
solution: copy your array to new array and set to state will solve issue

Related

REACT-SELECT defaultValue in CustomDropdown not working

I want the default value of my dropdown to be defaultValue={item.taste} (value from match.json) but it's not working... (go to /menu/Menu1 and Pea Soup)
import Select from "react-select";
export default function CustomDropdown({ style, options, defaultValue }) {
return (
<div style={style}>
<Select options={options} defaultValue={defaultValue} />
</div>
);
}
MenuItemDisplay:
export default function MenuItemDisplay() {
const { menuId, itemId } = useParams();
const { match } = JsonData;
const matchData = match.find((el) => el._id_menu === menuId)?._ids ?? [];
const item = matchData.find((el) => el._id === itemId);
const styles = {
select: {
width: "100%",
maxWidth: 150
}
};
const TASTE = [
{ label: "Good", value: "Good" },
{ label: "Medium", value: "Medium" },
{ label: "Bad", value: "Bad" }
];
...
return (
<>
<div className="TextStyle">
{"Taste "}
<CustomDropdown
style={styles.select}
options={TASTE}
defaultValue={item.taste}
//The default value is not working only if it's
//TASTE[0]
/>
</div>
...
</>
);
}
Here the link for the code
As defaultValue you need to pass one of the objects of the TASTE array. You can do this:
<CustomDropdown
style={styles.select}
options={TASTE}
defaultValue={TASTE.find(t => t.label === item.taste)}
/>

How do I update deeply nested array with hooks in react?

I have a nested array of objects, each object have a nested options array like this.
const [formFields, setFormFields ] = useState({
formTitle: '',
fields: [
{name: 'country', val: '', type: 'radio', options: ['Japan', 'Korea', 'usa'] },
{name: 'state', val: '', type: 'select', options: ['texas', 'florida']},
{name: 'location', val: '', type: 'text', options: []},
]})
Each of the items in the nested options array is supposed to be a value in a textInput which is editable.
I want to be able to add/remove/edit these values inside the textInput with a button click.
Please how will I be able to achieve this?
my code
<Containter>
{formFields.fields.map((field, index) => (
<View key={index}>
<View>
<TextInput
onChangeText={(value ) => {
onChange({name: field.name, value });
}}
value={field.name}
/>
</View>
{(field.type === 'select' || field.type === 'radio') && (
<>
{field.options.map((option) => (
<TextInput value={option}
onChangeText={(value ) => {
onChange({name: field.options, ...field.options, value });
}}
/>
<Text onPress={removeOption}>X</Text>
))}
<Button title="add option" />
</>
)
}
<IconButton
icon="delete"
onPress={handleRemoveField}
/>
</View>
))}
<Button
onPress={handleAddField}
title="Add"
/>
</Containter>
Add & remove implementation:
onAdd (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = [...field.options,value]
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}
onRemove (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = field.options.filter((item) => item != value)
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}
// in constructor
this.onChange = this.onChange.bind(this)
// in class
onChange (index,value) {
this.setState(state => {
const fields = state.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
return {
...state,
fields
}
})
}
// in component
onChangeText( (e) => onChange(index, e.target.value) )
For value changing:
onChange (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
setFormFields({
...formFields,
fields
})
}
...
// somewhere in input element
<TextInput ... onChangeText={(e) => onChange(index,e.target.value)} .. />

How to make several buttons simultaneously active but with some conditions?

I have a problem. I want to make buttons section, where user can click buttons to filter some content. When user click on 'all' button, all other should be turn off (change its color to initial, not active) in this moment. Also, user can check multiple buttons.
I can't get how to do this.
Example of JSON:
{
title: 'All',
id: 53,
},
{
title: 'Im a parent',
icon: <Parent />,
id: 0,
},
{
title: 'I live here',
icon: <ILiveHere />,
id: 2,
},
example of code: https://codesandbox.io/s/sleepy-haze-35htx?file=/src/App.js
Its wrong, I know. I tried some solutions, but I guess I can't get how to do it correctly.
With this code I can do active multiple buttons, but I can't get how to make conditions like
if (item.title === 'all){
TURN_OFF_ANY_OTHER_BTNS
}
I guess I should store checked buttons in temporary array to make these operations.
Will be really thankfull for help.
Is this something you would like?
const SocialRole = ({ item, selected, setSelected }) => {
let style =
[...selected].indexOf(item.id) !== -1
? { color: "red" }
: { color: "blue" };
return (
<button
style={style}
onClick={() => {
if (item.id === 53) {
setSelected(null);
} else {
setSelected(item.id);
}
}}
>
{item.icon}
<h1>{item.title}</h1>
</button>
);
};
export default function App() {
// We keep array of selected item ids
const [selected, setSelected] = useState([roles[0]]);
const addOrRemove = (item) => {
const exists = selected.includes(item);
if (exists) {
return selected.filter((c) => {
return c !== item;
});
} else {
return [...selected, item];
}
};
return (
<div>
{roles.map((item, index) => (
<SocialRole
key={index}
item={item}
selected={selected}
setSelected={(id) => {
if (id === null) setSelected([]);
else {
setSelected(addOrRemove(id));
}
}}
/>
))}
</div>
);
}
If I understand your problem, I think this is what you are looking for:
const roles = [
{
title: "All",
id: 53
},
{
title: "I live here",
id: 0
},
{
title: "I live here too",
id: 2
}
];
// button
const SocialRole = ({ item, selected, setSelected }) => {
const isActive = selected === item.title || selected === 'All';
return (
<button
style={isActive ? { color: "red" } : { color: "blue" }}
onClick={() => setSelected(item.title)}
>
{item.icon}
<h1>{item.title}</h1>
</button>
);
};
export default function App() {
const [selected, setSelected] = useState(roles[0].title);
return (
<div>
{roles.map((item, index) => (
<SocialRole
key={index}
item={item}
selected={selected}
setSelected={setSelected}
/>
))}
</div>
);
}
The problem was you were setting a new state into each button, when you should just use the state from the App.

CheckMap State not updating when unticking checkbox in React

This is my first time to develop a react application. Please bear with me.
I intended to display the key (value in the table) on a confirmation dialog. It works as intended but when I tick then untick the checkbox, it seems that the key is still in the map.
I displayed checkedMap by retrieving its keys and add it in an array. But when I try to untick and then invoke the dialog, checkedMap still has the unticked key, in the debug view.
Thank you for your help.
import React, { useState, useEffect, useRef } from "react";
//import makeData from "../makeData";
import { useTableState } from "react-table";
import Table from "../TransactionPanelTable";
// Simulate a server
const getServerData = async ({ filters, sortBy, pageSize, pageIndex }) => {
await new Promise(resolve => setTimeout(resolve, 500));
// Ideally, you would pass this info to the server, but we'll do it here for convenience
const filtersArr = Object.entries(filters);
// Get our base data
let rows = [];
rows.push({
transaction_seq: 1555,
record_count: 300,
user_id: "test1",
updated_at: "09-MAY-19 10.01.45.371373000 PM",
duration: 5.7
});
rows.push({
transaction_seq: 2666,
rec_count: 1234,
user_id: "test2",
updated_at: "",
duration: 1.23
});
// Apply Filters
if (filtersArr.length) {
rows = rows.filter(row =>
filtersArr.every(([key, value]) => row[key].includes(value))
);
}
// Apply Sorting
if (sortBy.length) {
const [{ id, desc }] = sortBy;
rows = [...rows].sort(
(a, b) => (a[id] > b[id] ? 1 : a[id] === b[id] ? 0 : -1) * (desc ? -1 : 1)
);
}
// Get page counts
const pageCount = Math.ceil(rows.length / pageSize);
const rowStart = pageSize * pageIndex;
const rowEnd = rowStart + pageSize;
// Get the current page
rows = rows.slice(rowStart, rowEnd);
return {
rows,
pageCount
};
};
export default function({ infinite }) {
**const [checkedMap, setCheckedMap] = useState(new Map());**
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const currentRequestRef = useRef();
**let newMap = new Map();**
const fetchData = async () => {
setLoading(true);
// We can use a ref to disregard any outdated requests
const id = Date.now();
currentRequestRef.current = id;
// Call our server for the data
const { rows, pageCount } = await getServerData({
filters,
sortBy,
pageSize,
pageIndex
});
// If this is an outdated request, disregard the results
if (currentRequestRef.current !== id) {
return;
}
// Set the data and pageCount
setData(rows);
setState(old => ({
...old,
pageCount
}));
**rows.forEach(row => newMap.set(row, false));**
//setCheckedMap(newMap);
setLoading(false);
};
**const handleCheckedChange = transaction_seq => {
let modifiedMap = checkedMap;
modifiedMap.set(transaction_seq, !checkedMap.get(transaction_seq));
setCheckedMap(modifiedMap);
};**
const columns = [
{
Header: "Transaction(s)",
className: "left",
columns: [
{
id: "checkbox",
accessor: "checkbox",
Cell: ({ row }) => {
return (
<input
type="checkbox"
className="checkbox"
checked={checkedMap.get(row.original.transaction_seq)}
onChange={() =>
handleCheckedChange(row.original.transaction_seq)
}
/>
);
},
sortable: false,
width: 45
},
{
Header: "Transaction Sequence",
accessor: "transaction_seq",
id: "transaction_seq",
minWidth: 200,
maxWidth: 300
},
{
Header: "Record count",
accessor: "record_count",
width: 300
},
{
Header: "User Id",
accessor: "user_id",
width: 300
},
{
Header: "Updated At",
accessor: "updated_at",
width: 400
},
{
Header: "Duration",
accessor: "duration",
width: 400
}
]
}
];
// Make a new controllable table state instance
const state = useTableState({ pageCount: 0 });
const [{ sortBy, filters, pageIndex, pageSize }, setState] = state;
// When sorting, filters, pageSize, or pageIndex change, fetch new data
useEffect(() => {
fetchData();
}, [sortBy, filters, pageIndex, pageSize]);
return (
<React.Fragment>
<Table
{...{
data,
**checkedMap,**
columns,
infinite,
state, // Pass the state to the table
loading,
manualSorting: true, // Manual sorting
manualFilters: true, // Manual filters
manualPagination: true, // Manual pagination
disableMultiSort: true, // Disable multi-sort
disableGrouping: true, // Disable grouping
debug: true
}}
/>
</React.Fragment>
);
}
Here is the table.js
import styled, { css } from "styled-components";
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
import { FixedSizeList as List } from "react-window";
import {
useTable,
useColumns,
useRows,
useFilters,
useSortBy,
useExpanded,
usePagination,
useFlexLayout
} from "react-table";
export default function MyTable({ loading, infinite, checkedMap, ...props }) {
const instance = useTable(
{
...props
},
useColumns,
useRows,
useFilters,
useSortBy,
useExpanded,
usePagination,
useFlexLayout
);
const {
getTableProps,
headerGroups,
rows,
getRowProps,
pageOptions,
page,
state: [{ pageIndex, pageSize, sortBy, filters }],
gotoPage,
prepareRow,
previousPage,
nextPage,
setPageSize,
canPreviousPage,
canNextPage
} = instance;
const { listRef, rowHeight, height, overscan } = useInfiniteScroll({
enabled: infinite,
sortBy,
filters,
pageIndex,
pageSize
});
let tableBody;
const renderRow = (row, index, style = {}) => {
if (!row) {
return (
<Row {...{ style, even: index % 2 }}>
<Cell>Loading more...</Cell>
</Row>
);
}
prepareRow(row);
return (
<Row {...row.getRowProps({ style, even: index % 2 })}>
{row.cells.map(cell => {
const isPivot = row.groupByID === cell.column.id;
const showAggregate = row.subRows && !isPivot;
return (
<Cell {...cell.getCellProps()}>
{showAggregate ? (
cell.column.aggregate ? (
cell.render("Aggregated")
) : null
) : (
<span>
{isPivot ? (
<span
style={{
cursor: "pointer",
paddingLeft: `${row.depth * 2}rem`,
paddingRight: "1rem",
whiteSpace: "nowrap"
}}
onClick={() => row.toggleExpanded()}
/>
) : null}
{cell.render("Cell")}
{isPivot ? <span> ({row.subRows.length})</span> : null}
</span>
)}
</Cell>
);
})}
</Row>
);
};
if (infinite) {
tableBody = (
<List
ref={listRef}
height={height}
itemCount={rows.length + 1}
itemSize={rowHeight}
overscanCount={overscan}
scrollToAlignment="start"
{...getRowProps()}
>
{({ index, style }) => {
const row = rows[index];
return renderRow(row, index, style);
}}
</List>
);
} else {
tableBody =
page && page.length ? page.map((row, i) => renderRow(row, i)) : null;
}
let pagination;
pagination = pageOptions.length ? (
<Pagination {...getRowProps()}>
<Cell>
<Button onClick={() => previousPage()} disabled={!canPreviousPage}>
Previous
</Button>{" "}
<Button onClick={() => nextPage()} disabled={!canNextPage}>
Next
</Button>{" "}
<span>
Page{" "}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{" "}
</span>
<span>
| Go to page:{" "}
<Input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
gotoPage(page);
}}
style={{ width: "100px" }}
/>
</span>{" "}
<Select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</Select>{" "}
<Button onClick={() => reprocessConfirmation()}>Reprocess</Button>
</Cell>
</Pagination>
) : null;
function reprocessConfirmation() {
let confirmation = window.confirm(
"Do you want to reprocess transaction sequence " +
Array.from(checkedMap.keys())
);
if (confirmation === true) console.log(Array.from(checkedMap.keys()));
else console.log("CANCEL");
}
return (
<React.Fragment>
<Table {...getTableProps()}>
{headerGroups.map(headerGroup => (
<HeaderRow {...headerGroup.getRowProps()}>
{headerGroup.headers.map(column => (
<Header
{...column.getHeaderProps()}
sorted={column.sorted}
sortedDesc={column.sortedDesc}
sortedIndex={column.sortedIndex}
>
<div>
<span {...column.getSortByToggleProps()}>
{column.render("Header")}
</span>{" "}
</div>
{column.canFilter ? <div>{column.render("Filter")}</div> : null}
</Header>
))}
</HeaderRow>
))}
{tableBody}
<Row {...getRowProps()}>
{loading ? (
<Cell>
<strong>Loading...</strong>
</Cell>
) : (
<Cell>{rows.length} Total Records</Cell>
)}
</Row>
{pagination}
</Table>
</React.Fragment>
);
}

React syntax does not work

componentDidMount() {
const restaurants = Restaurant.all()
restaurants.then( rests => {
this.setState({
restaurants: rests
})
})
}
render() {
const { restaurants } = this.state;
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
restaurants.map( restaurant => {
return restaurant ?
( <option key={restaurant.id}>{ restaurant.name }</option> )
:
''
})
}
</Chosen>
</main>
);
}
I have my react code above and trying to return a mapped array that is supposed to be something like
[<option key={1}>first</option>, <option key={2}>two</option>, <option key={3}>three</option>]
It works if I put a randomly created array like this,
render() {
const { restaurants } = this.state;
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
[<option key={1}>first</option>, <option key={2}>two</option>, <option key={3}>three</option>]
}
</Chosen>
</main>
);
}
but no matter what I do with the map method, it just doesn't show anything.
I have already checked there is an array containing elements assigned to this.state.restaurant.
componentDidMount is called after the first render. Consequently your restaurants is undefined when the first render processed.
You can check if restaurants exists in render method:
componentDidMount() {
const restaurants = Restaurant.all()
restaurants.then( rests => {
this.setState({
restaurants: rests
})
})
}
render() {
const { restaurants } = this.state;
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
restaurants && restaurants.map( restaurant => {
return restaurant ?
( <option key={restaurant.id}>{ restaurant.name }</option> )
:
null
})
}
</Chosen>
</main>
);
}
Also, check if your state is defined in the constructor or as the class property.
So the whole component could be the follow:
class Rests extends React.Component {
state = {restaurants: null};
componentDidMount() {
const restaurants = Restaurant.all()
restaurants.then( rests => {
this.setState({
restaurants: rests
})
})
}
render() {
const { restaurants } = this.state;
if (!restaurants) {
return null; // or you can return <LoadingSpinner /> here
}
return (
<main className="SearchRestaurantsPage" style={{textAlign: 'center'}}>
<Chosen className="Chosen-select" onChange={ value => console.log(value) }>
{
restaurants.map( restaurant => {
return restaurant ?
( <option key={restaurant.id}>{ restaurant.name }</option> )
:
null
})
}
</Chosen>
</main>
);
}
}
In the last example, we render nothing if there is no any data in restaurants. After we fetch data we rerender component and show options to users
The issue might be where you declared the restaurent constant from the state. I've written a sample code that works below.
import React from 'react';
const restaurentData = [
{
id: 1,
name: 'name 1'
},
{
id: 2,
name: 'name 2'
},
{
id: 3,
name: 'name 3'
}
]
class Hello extends React.Component {
constructor() {
super();
this.state = {
restaurents: restaurentData
}
}
render () {
const restaurents = this.state.restaurents;
return (
<ul>
{restaurents.map(restaurent => {
return restaurent
? (<li key={restaurent.id}>{`${restaurent.id} -- ${restaurent.name}`} </li>)
: null;
})}
</ul>
)
}
}
export default Hello;

Categories