I'm currently facing a problem where the screen doesnt re-render to load the new added value to array. Even tried with componentWillReceiveProps and trigger a this.forceUpdate() manually doesn't help. The screen only show the new value when I restart the app
this.props.addNewRecord({
recordDetails: { ...this.props.recordDetails, type: this.state.type },
records: this.props.records
});
return (
<View>
{
this.props.records.map((x, i) => {
return (
<View style={{ flexDirection: 'row' }}>
<Text>{`${i}: `}</Text>
<Text>{x.id}</Text>
</View>
);
})
}
</View>
);
const mapStateToProps = ({ account, settings, main }) => {
const year = moment().year();
const month = moment().format('MMM');
return {
records: account.records[year][month].records,
};
};
export default connect(mapStateToProps)(SummaryScreen);
account.records has the following structure
{
2018: {
Jan: {
records: [{...}, {...}]
}
}
}
And below is the adding record part
const addNewRecord = ({ recordDetails, records }) => {
const year = moment(recordDetails.date).year();
const month = moment(recordDetails.date).format('MMM');
const newRec = {
id: uuidv4(),
account_id: recordDetails.account_id,
date: recordDetails.date,
description: recordDetails.description,
amount: recordDetails.result,
type: recordDetails.type
};
update(year, month, newRec, records);
return {
type: ADD_NEW_RECORD,
payload: records
};
};
const update = (year, month, obj, target) => {
[year, month].reduce((r, e, i, a) => {
if (!a[i + 1] && r[e]) {
r[e].records.push(obj);
if (obj.type === 'expense') r[e].totalExpenses = parseFloat(r[e].totalExpenses) + parseFloat(obj.amount);
if (obj.type === 'income') r[e].totalIncome = parseFloat(r[e].totalIncome) + parseFloat(obj.amount);
}
return r[e] = (r[e] || (a[i + 1] ? {} : {
records: [obj],
totalExpenses: obj.type === 'expense' ? parseFloat(obj.amount) : 0,
totalIncome: obj.type === 'income' ? parseFloat(obj.amount) : 0,
type: obj.type
}));
}, target);
};
And Reducer as below:
switch (action.type) {
case ADD_NEW_RECORD:
return { ...state, records: action.payload };
this.props.addNewRecord({
recordDetails: { ...this.props.recordDetails, type: this.state.type },
records: _.cloneDeep(this.props.records)//this.props.records
});
The problem is I've passed this.props.records as records which later gets mutated, and eventually old state get mutated to be the same as new state and that's why Redux unable to spot the differences.
Credits to #Y.Gherbi for spotted this flaw. Hope it helps whoever facing same issue
Related
I thinking for few days but cant realize how can i make it. I have 4 json data and 4 picker.
Its for city,district,village,neirborhood. In first i must choose city then in second picker it must show district about that i choose city. When i choose district from picker third one must show villages about that district. And neirborhood is same too. In that json datas they have some connection. Like city json have ' id-name' district have 'id-cityid-name' village have 'id-districtid-name' neirborhood have 'id-villageid-name' Like that. But i cant figure out how can i make it. Its my codes I really stuck with that hardly i need some help please. Thank you! My codes :
Elements :
const DropdownElements = [
{
key: 1,
title: "Şehir",
placeholder: "Şehir Seçiniz",
apiUrl: "https://api.npoint.io/995de746afde6410e3bd",
type: "city",
selecteditem: "",
data : [],
},
{
key: 2,
title: "İlçe",
placeholder: "İlçe Seçiniz",
apiUrl: "https://api.npoint.io/fc801dbd3fc23c2c1679",
type: "district",
selecteditem: "",
data : [],
},
{
key: 3,
title: "Köy",
placeholder: "Köy Seçiniz",
apiUrl: "https://api.npoint.io/72cf025083b70615b8bb",
type: "village",
selecteditem: "",
data : [],
},
{
key: 4,
title: 'Mahalle',
placeholder:'Mahalle Seçiniz',
apiUrl: 'https://api.npoint.io/0c04c63923c8ca4e117b',
type: 'neighborhood',
selecteditem: "",
data : [],
},
];
Component :
const PickerCompanent = (props) => {
const [xdata, setData] = useState([]);
const [newData, setNewData] = useState([]);
let x;
let y = [];
// data.filter((a) => a.il_id == "3");
useEffect(() => {
props.datasource.then(setData);
switch (props.type) {
case "city":
x = props.selecteditem;
setNewData(xdata);
break;
case "district":
y = xdata.filter((element) => {
if (props.selecteditem === element.id) {
return element;
}
});
break;
case "village":
console.log("village");
break;
default:
console.log("def");
break;
}
}, [props.datasource]);
return (
<Select
showSearch
style={{ width: 200, marginLeft: 15 }}
placeholder={props.placeholder}
optionFilterProp="children"
onChange={(x) => props.onChange(x)}
onFocus={props.onFocus()}
datasource={xdata}
onSearch={props.onSearch()}
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
>
{xdata &&
xdata.map((x) => {
return (
<Select.Option key={x.id} value={x.id}>
{x.name}{" "}
</Select.Option>
);
})}
</Select>
);
};
My App :
const App = () => {
const [dataap, setDataAp] = useState([]);
const [idhold, setIDHold] = useState();
const filldata = (value) => {};
function onChange(value) {
setIDHold(value);
console.log(value);
}
const getData = (value, type) => {
return fetch(value)
.then((x) => x.json())
.then((y) => {
return y;
});
};
function onFocus() {}
function onSearch(val) {}
return (
<Space>
{DropdownElements.map((x) => {
return (
<PickerCompanent
showSearch
selecteditem={idhold}
key={x.key}
placeholder={x.placeholder}
type={x.type}
datasource={getData(x.apiUrl)}
onFocus={onFocus}
onChange={(z) => onChange(z)}
onFocus={onFocus}
onSearch={onSearch}
></PickerCompanent>
);
})}
</Space>
);
};
If you need i can give my teamviewer or skype too. I really need that help thanks for replies!
Sandbox : codesandbox.io/s/runtime-monad-vxit
https://codesandbox.io/s/mystifying-moore-7w105?file=/src/App.js
Select CityTwo to see the dropdown update.
You need a switch. Updating arrays inside state is tricky. You can't populate or push anything in an array that's in state. Update your array outside state, THEN update state.
I want to add new Objects when user click on checkbox. For example , When user click on group , it will store data {permission:{group:["1","2"]}}. If I click on topgroup , it will store new objects with previous one
{permission:{group:["1","2"]},{topGroup:["1","2"]}}.
1st : The problem is that I can not merge new object with previous one . I saw only one objects each time when I click on the group or topgroup.
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
Object.assign(prevState.permission, { [value]: this.state.checked });
});
});
};
<CheckboxGroup
options={options}
value={checked}
onChange={this.onChange(this.props.label)}
/>
Here is my codesanbox:https://codesandbox.io/s/stackoverflow-a-60764570-3982562-v1-0qh67
It is a lot of code because I've added set and get to set and get state. Now you can store the path to the state in permissionsKey and topGroupKey. You can put get and set in a separate lib.js.
In this example Row is pretty much stateless and App holds it's state, this way App can do something with the values once the user is finished checking/unchecking what it needs.
const Checkbox = antd.Checkbox;
const CheckboxGroup = Checkbox.Group;
class Row extends React.Component {
isAllChecked = () => {
const { options, checked } = this.props;
return checked.length === options.length;
};
isIndeterminate = () => {
const { options, checked } = this.props;
return (
checked.length > 0 && checked.length < options.length
);
};
render() {
const {
options,
checked,
onChange,
onToggleAll,
stateKey,
label,
} = this.props; //all data and behaviour is passed by App
return (
<div>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={e =>
onToggleAll(e.target.checked, stateKey)
}
checked={this.isAllChecked()}
>
Check all {label}
</Checkbox>
<CheckboxGroup
options={options}
value={checked}
onChange={val => {
onChange(stateKey, val);
}}
/>
</div>
</div>
);
}
}
//helper from https://gist.github.com/amsterdamharu/659bb39912096e74ba1c8c676948d5d9
const REMOVE = () => REMOVE;
const get = (object, path, defaultValue) => {
const recur = (current, path) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(current[path[0]], path.slice(1));
};
return recur(object, path);
};
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path) => {
if (path.length === 1) {
return setKey(
current,
path[0],
callback(current[path[0]])
);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1))
);
};
return recur(object, path, callback);
};
class App extends React.Component {
state = {
permission: { group: [] },
topGroup: [],
some: { other: [{ nested: { state: [] } }] },
};
permissionsKey = ['permission', 'group']; //where to find permissions in state
topGroupKey = ['topGroup']; //where to find top group in state
someKey = ['some', 'other', 0, 'nested', 'state']; //where other group is in state
onChange = (key, value) => {
//use set helper to set state
this.setState(set(this.state, key, arr => value));
};
isIndeterminate = () =>
!this.isEverythingChecked() &&
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result || get(this.state, key).length,
false
);
toggleEveryting = e => {
const checked = e.target.checked;
this.setState(
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
set(result, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
),
this.state
)
);
};
onToggleAll = (checked, key) => {
this.setState(
//use set helper to set state
set(this.state, key, () =>
checked
? this.plainOptions.map(({ value }) => value)
: []
)
);
};
isEverythingChecked = () =>
[
this.permissionsKey,
this.topGroupKey,
this.someKey,
].reduce(
(result, key) =>
result &&
get(this.state, key).length ===
this.plainOptions.length,
true
);
plainOptions = [
{ value: 1, name: 'Apple' },
{ value: 2, name: 'Pear' },
{ value: 3, name: 'Orange' },
];
render() {
return (
<React.Fragment>
<h1>App state</h1>
{JSON.stringify(this.state)}
<div>
<Checkbox
indeterminate={this.isIndeterminate()}
onChange={this.toggleEveryting}
checked={this.isEverythingChecked()}
>
Toggle everything
</Checkbox>
</div>
{[
{ label: 'group', stateKey: this.permissionsKey },
{ label: 'top', stateKey: this.topGroupKey },
{ label: 'other', stateKey: this.someKey },
].map(({ label, stateKey }) => (
<Row
key={label}
options={this.plainOptions}
// use getter to get state selected value
// for this particular group
checked={get(this.state, stateKey)}
label={label}
onChange={this.onChange} //change behaviour from App
onToggleAll={this.onToggleAll} //toggle all from App
//state key to indicate what state needs to change
// used in setState in App and passed to set helper
stateKey={stateKey}
/>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.0.3/antd.js"></script>
<div id="root"></div>
I rewrite all the handlers.
The bug in your code is located on the usage of antd Checkbox.Group component with map as a child component, perhaps we need some key to distinguish each of the Row. Simply put them in one component works without that strange state update.
As the demand during communication, the total button is also added.
And, we don't need many states, keep the single-source data is always the best practice.
import React from "react";
import ReactDOM from "react-dom";
import "antd/dist/antd.css";
import "./index.css";
import { Checkbox } from "antd";
const group = ["group", "top"];
const groupItems = ["Apple", "Pear", "Orange"];
const CheckboxGroup = Checkbox.Group;
class App extends React.Component {
constructor() {
super();
this.state = {
permission: {}
};
}
UNSAFE_componentWillMount() {
this.setDefault(false);
}
setDefault = fill => {
const temp = {};
group.forEach(x => (temp[x] = fill ? groupItems : []));
this.setState({ permission: temp });
};
checkLength = () => {
const { permission } = this.state;
let sum = 0;
Object.keys(permission).forEach(x => (sum += permission[x].length));
return sum;
};
/**
* For total
*/
isTotalIndeterminate = () => {
const len = this.checkLength();
return len > 0 && len < groupItems.length * group.length;
};
onCheckTotalChange = () => e => {
this.setDefault(e.target.checked);
};
isTotalChecked = () => {
return this.checkLength() === groupItems.length * group.length;
};
/**
* For each group
*/
isIndeterminate = label => {
const { permission } = this.state;
return (
permission[label].length > 0 &&
permission[label].length < groupItems.length
);
};
onCheckAllChange = label => e => {
const { permission } = this.state;
const list = e.target.checked ? groupItems : [];
this.setState({ permission: { ...permission, [label]: list } });
};
isAllChecked = label => {
const { permission } = this.state;
return !groupItems.some(x => !permission[label].includes(x));
};
/**
* For each item
*/
isChecked = label => {
const { permission } = this.state;
return permission[label];
};
onChange = label => e => {
const { permission } = this.state;
this.setState({ permission: { ...permission, [label]: e } });
};
render() {
const { permission } = this.state;
console.log(permission);
return (
<React.Fragment>
<Checkbox
indeterminate={this.isTotalIndeterminate()}
onChange={this.onCheckTotalChange()}
checked={this.isTotalChecked()}
>
Check all
</Checkbox>
{group.map(label => (
<div key={label}>
<div className="site-checkbox-all-wrapper">
<Checkbox
indeterminate={this.isIndeterminate(label)}
onChange={this.onCheckAllChange(label)}
checked={this.isAllChecked(label)}
>
Check all
</Checkbox>
<CheckboxGroup
options={groupItems}
value={this.isChecked(label)}
onChange={this.onChange(label)}
/>
</div>
</div>
))}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById("container"));
Try it online:
Please try this,
onChange = value => checked => {
this.setState({ checked }, () => {
this.setState(prevState => {
permission : { ...prevSatate.permission , { [value]: this.state.checked }}
});
});
};
by using spread operator you can stop mutating the object. same way you can also use object.assign like this.
this.setState(prevState => {
permission : Object.assign({} , prevState.permission, { [value]: this.state.checked });
});
And also i would suggest not to call setState in a callback. If you want to access the current state you can simply use the current checked value which you are getting in the function itself.
so your function becomes ,
onChange = value => checked => {
this.setState({ checked });
this.setState(prevState => {return { permission : { ...prevSatate.permission, { [value]: checked }}
}});
};
Try the following
//Inside constructor do the following
this.state = {checkState:[]}
this.setChecked = this.setChecked.bind(this);
//this.setChecked2 = this.setChecked2.bind(this);
//Outside constructor but before render()
setChecked(e){
this.setState({
checkState : this.state.checkState.concat([{checked: e.target.id + '=>' + e.target.value}])
//Id is the id property for a specific(target) field
});
}
//Finally attack the method above.i.e. this.setChecked to a form input.
Hope it will address your issues
I want to filter the data from my multiple states at one time. But I am getting the data of only second state.
I have two states and both states are getting seprate data from seprate apis. Now I want to filter the data from it. thank youI don't know what i m doing wrong so pls help me and look at my code.
searchFeatured = value => {
const filterFeatured = (
this.state.latestuploads || this.state.featuredspeakers
).filter(item => {
let featureLowercase = (item.name + " " + item.title).toLowerCase();
let searchTermLowercase = value.toLowerCase();
return featureLowercase.indexOf(searchTermLowercase) > -1;
});
this.setState({
featuredspeakers: filterFeatured,
latestuploads: filterFeatured
});
};
class SearchPage extends Component {
constructor(props) {
super(props);
this.state = {
loading: false,
featuredspeakers: [],
latestuploads: [],
};
}
componentDidMount() {
axios
.all([
axios.get(
'https://staging.islamicmedia.com.au/wp-json/islamic-media/v1/featured/speakers',
),
axios.get(
'https://staging.islamicmedia.com.au/wp-json/islamic-media/v1/featured/latest-uploads',
),
])
.then(responseArr => {
//this will be executed only when all requests are complete
this.setState({
featuredspeakers: responseArr[0].data,
latestuploads: responseArr[1].data,
loading: !this.state.loading,
});
});
}
Using the || (OR) statement will take the first value if not null/false or the second. What you should do is combine the arrays
You should try something like
[...this.state.latestuploads, ... this.state.featuredspeakers].filter(item=>{});
Ahmed, I couldn't get your code to work at all - searchFeatured is not called anywhere. But I have some thoughts, which I hope will help.
I see that you're setting featuredspeakers and latestuploads in componentDidMount. Those are large arrays with lots of data.
But then, in searchFeatured, you are completely overwriting the data that you downloaded and replacing it with search/filter results. Do you really intend to do that?
Also, as other people mentioned, your use of the || operator is just returning the first array, this.state.latestuploads, so only that array is filtered.
One suggestion that might help is to set up a very simple demo class which only does the filtering that you want. Don't use axios at all. Instead, set up the initial state with some mocked data - an array of just a few elements. Use that to fix the filter and search functionality the way that you want. Here's some demo code:
import React from 'react';
import { Button, View, Text } from 'react-native';
class App extends React.Component {
constructor(props) {
super(props);
this.searchFeatured = this.searchFeatured.bind(this);
this.customSearch = this.customSearch.bind(this);
this.state = {
loading: false,
featuredspeakers: [],
latestuploads: [],
};
}
searchFeatured = value => {
// overwrite featuredspeakers and latestuploads! Downloaded data is lost
this.setState({
featuredspeakers: this.customSearch(this.state.featuredspeakers, value),
latestuploads: this.customSearch(this.state.latestuploads, value),
});
};
customSearch = (items, value) => {
let searchTermLowercase = value.toLowerCase();
let result = items.filter(item => {
let featureLowercase = (item.name + " " + item.title).toLowerCase();
return featureLowercase.indexOf(searchTermLowercase) > -1;
});
return result;
}
handlePress(obj) {
let name = obj.name;
this.searchFeatured(name);
}
handleReset() {
this.setState({
featuredspeakers: [{ name: 'Buffy', title: 'Slayer' }, { name: 'Spike', title: 'Vampire' }, { name: 'Angel', title: 'Vampire' }],
latestuploads: [{ name: 'Sarah Michelle Gellar', 'title': 'Actress' }, { name: 'David Boreanaz', title: 'Actor' }],
loading: !this.state.loading,
});
}
componentDidMount() {
this.handleReset();
}
getList(arr) {
let output = [];
if (arr) {
arr.forEach((el, i) => {
output.push(<Text>{el.name}</Text>);
});
}
return output;
}
render() {
let slayerList = this.getList(this.state.featuredspeakers);
let actorList = this.getList(this.state.latestuploads);
return (
<View>
<Button title="Search results for Slayer"
onPress={this.handlePress.bind(this, {name: 'Slayer'})}></Button>
<Button title="Search results for Actor"
onPress={this.handlePress.bind(this, {name: 'Actor'})}></Button>
<Button title="Reset"
onPress={this.handleReset.bind(this)}></Button>
<Text>Found Slayers?</Text>
{slayerList}
<Text>Found Actors?</Text>
{actorList}
</View>
);
}
};
export default App;
You should apply your filter on the lists separately then. Sample code below =>
const searchFeatured = value => {
this.setState({
featuredspeakers: customSearch(this.state.featuredspeakers, value),
latestuploads: customSearch(this.state.latestuploads, value)
});
};
const customSearch = (items, value) => {
return items.filter(item => {
let featureLowercase = (item.name + " " + item.title).toLowerCase();
let searchTermLowercase = value.toLowerCase();
return featureLowercase.indexOf(searchTermLowercase) > -1;
});
}
I have the following structure in my Redux state:
"equipment" : {
"l656k75ny-r7w4mq" : {
"id" : "l656k75ny-r7w4mq",
"image" : "cam.png",
"isChecked" : false,
"list" : {
"2taa652p5-mongp0" : {
"id" : "2taa652p5-mongp0",
"image" : "dst.png",
"isChecked" : false,
"name" : "Dst"
},
"y3ds1rspk-ftdg2t" : {
"id" : "y3ds1rspk-ftdg2t",
"image" : "flm.png",
"isChecked" : false,
"name" : "Flm"
}
},
"name" : "EQ 1"
},
"lpbixy0f7-bmiwzl" : {
"image" : "prm.png",
"isChecked" : false,
"list" : {
"l40snp2y6-o7gudg" : {
"image" : "tlp.png",
"isChecked" : false,
"name" : "Tlp"
},
"qmxf9bn3v-9x1nky" : {
"image" : "prm.png",
"isChecked" : false,
"name" : "Prm"
}
},
"name" : "EQ 2"
}
}
I have an Equipment component (sort of higher order component), in which I subscribe to this part of the state and I pass this its data to my CustomExpansionPanel component:
class Equipment extends Component {
//...different methods
render() {
const { data } = this.props;
let content = error ? <p>Oops, couldn't load equipment list...</p> : <Spinner />;
if ( data.length > 0 ) {
content = (
<Fragment>
<CustomExpansionPanel data={data}/>
</Fragment>
);
}
return (
<SectionContainer>
{content}
</SectionContainer>
);
}
}
const mapStateToProps = state => {
return {
data: state.common.equipment,
};
};
connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Equipment));
In my CustomExpansionPanel I map the received data and create an expansion panel for each section, while passing this section to CustomTable component, which is displayed as ExpansionPanelDetails content.
class CustomExpansionPanel extends Component {
//... different methods
render() {
const { data } = this.props;
return (
<Fragment>
{
data.map((section, index) => {
return (
<ExpansionPanel defaultExpanded={false} key={section.id} disabled={!section.hasOwnProperty('list')} >
<ExpansionPanelSummary expandIcon={<StyledIcon classes={{root: classes.expansionIcon}} />}>
<Avatar
alt={section.name}
src={images('./' + section.image)}
classes={{root: classes.avatar}}
/>
<Typography variant='body1' classes={{root: classes.summaryText}}>{section.name}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<CustomTable data={section} imageVariant={imageVariant} parentId={section.id} />
</ExpansionPanelDetails>
</ExpansionPanel>
);
})
}
</Fragment>
);
}
}
And in my CustomTable component I map the list property of the received section and display its data. When clicking on the row I build a Form component with the appropriate fields and display it for editing the list item object:
class CustomTable extends Component {
//...local state
//...some methods
cellClickHandler = (object, index) => {
this.setState({
editedObject: object,
editedObjectIndex: index
}, () => {
this.setFormData(object);
});
};
handleSaveClicked = data => {
data.id = this.state.editedObject.id;
if(this.props.parentId) {
data.parentId = this.props.parentId;
}
if ( this.state.editedObject && this.state.editedObject.list ) data.list = this.state.editedObject.list;
this.props.onSaveItemEditClicked(this.props.updatedSection, data, this.state.editItemLevel, this.state.editedObjectIndex, () => {
this.handleDialogState();
});
};
componentWillMount() {
if ( this.props.data.list ) {
if ( Array.isArray(this.props.data.list) ) {
this.setState({rows: [...this.props.data]});
} else {
let transformedData = [];
for (let [key, value] of Object.entries(this.props.data.list)) {
let obj = value;
obj['id'] = key;
transformedData.push(obj);
}
this.setState({
rows: [...transformedData],
sortedProperties: sortProperties(Object.keys(transformedData[0]))
});
}
} else if (Array.isArray(this.props.data) && this.props.data.length) {
this.setState({
rows: [...this.props.data],
sortedProperties: sortProperties(Object.keys(this.props.data[0]))
});
}
if (this.props.parentId) {
this.setState({editItemLevel: 'inner'})
}
}
componentWillReceiveProps(nextProps, nextContext) {
if ( nextProps.data.list ) {
if ( Array.isArray(nextProps.data.list) ) {
this.setState({rows: [...nextProps.data]});
} else {
let transformedData = [];
for (let [key, value] of Object.entries(nextProps.data.list)) {
let obj = value;
obj['id'] = key;
transformedData.push(obj);
}
this.setState({
rows: [...transformedData],
sortedProperties: sortProperties(Object.keys(transformedData[0]))
});
}
} else if (Array.isArray(nextProps.data) && nextProps.data.length) {
this.setState({
rows: [...nextProps.data],
sortedProperties: sortProperties(Object.keys(nextProps.data[0]))
});
}
}
render() {
// Table, TableHead, TableRow, TableCell, TableFooter definitions...
<FormDialog
open={this.state.dialogOpen}
title={this.state.dialogTitle}
content={this.state.dialogContentText}
handleClose={this.handleDialogState}
formElements={this.state.formElements}
action={this.handleSaveClicked}
onCancelClicked={this.handleDialogState} />
}
}
const mapStateToProps = state => {
return {
updatedSection: state.control.Update.updatedSection
};
};
const mapDispatchToProps = dispatch => {
return {
onSaveItemEditClicked: (updatedSection, object, level, index, callback) => dispatch(commonActions.onSaveEdit(updatedSection, object, level, index, callback))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(CustomTable));
The form opens, I can change, for example, the name of the object and hit the save button, that triggers the onSaveItemEditClicked method, which dispatches the onSaveEdit action.
onSaveEdit = (updatedSection, object, level='root', index, callback) => {
return dispatch => {
dispatch(controlActions.onSetSnackbarMessage(SnackbarMessages.PROCESS.UPDATING + ' \'' + capitalize(object.name) + '\''));
dispatch(controlActions.DBProcessStart());
let parentId = null;
let url = '/' + updatedSection + '/' + object.id;
if (level === 'inner') {
url = '/' + updatedSection + '/' + object.parentId + '/list/' + object.id;
parentId = object.parentId;
delete object.parentId;
}
updateData(url, object)
.then(response => {
dispatch(controlActions.onSetSnackbarMessage('\'' + capitalize(object.name) + '\' ' + SnackbarMessages.SUCCESS.UPDATED_SUCCESSFULLY));
dispatch(controlActions.DBProcessSuccess());
if (parentId) {
object.parentId = parentId;
}
dispatch(this[saveEditSuccess](updatedSection, object, level, index));
if (callback != null) {
callback();
}
})
.catch(error => {
console.error('onSaveEdit error', error);
});
}
};
[saveEditSuccess] = (updatedSection, object, level='root', index) => {
return {
type: CommonCommands.UPDATE.SUCCESS,
section: updatedSection,
object: object,
level: level,
index: index
};
};
And finally that's my reducer:
const onUpdateSuccess = (state, action) => {
switch (action.level) {
case 'root':
let section = [...state[action.section]];
section = [
...section.slice(0, action.index),
action.object,
...section.slice(action.index + 1),
];
return updateObject(state, {
[action.section]: section
});
case 'inner':
console.log('inner update success', action);
section = [...state[action.section]];
let parentId = action.object.parentId;
let subsectionIndex = state[action.section].findIndex(member => member.id === parentId);
delete action.object.parentId;
let updatedObject = {...section[subsectionIndex].list[action.object.id], ...action.object};
section[subsectionIndex].list[action.object.id] = updatedObject;
return updateObject(state, {
[action.section]: [...section]
});
default:
return state;
}
};
This is the updateObject method:
const updateObject = (oldObject, updatedProperties) => {
return {
...oldObject,
...updatedProperties
};
};
Now if I inspect the state with Debug Redux tool, I can see the new name on the object, I see that the state does change. But the CustomTable component doesn't rerender, I still see the old name.
The exact same methodology works for me in every other case. If I have an array or object in a state property and I display it in the same CustomTable - I can edit it and see the results immediately (hence I have these 'root' and 'inner' separations). But not in this case.
I tried to use Object.assign({}, state, {[action.section]:section}); instead of updateObject method, but that also didn't help.
Any ideas how to make that component rerender with the changed properties?
I have an array of objects created in the new "Context API" like this ...
const reducer = (state, action) => {
switch (action.type) {
case "DELETE_CONTACT":
return {
...state,
contacts: state.contacts.filter(contact => {
return contact.id !== action.payload;
})
};
default:
return state;
}
};
export class Provider extends Component {
state = {
contacts: [
{
id: 1,
name: "John Doe",
email: "jhon.doe#site.com",
phone: "01027007024",
show: false
},
{
id: 2,
name: "Adam Smith",
email: "adam.smith#site.com",
phone: "01027007024",
show: false
},
{
id: 3,
name: "Mohammed Salah",
email: "mohammed.salah#site.com",
phone: "01027007024",
show: false
}
],
dispatch: action => {
this.setState(state => reducer(state, action));
}
};
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
);
}
}
I want to create an action in the "reducer" that allows me to edit each contact's "show" property based on its id that I will pass to the action as a payload, how can I do that?
To avoid array mutation and retain the element position while editing contact you can do this:
case "EDIT_CONTACT":
const { id, show } = action.payload;
const contact = { ...state.contacts.find(c => c.id === id), show };
return {
...state,
contacts: state.contacts.map(c => {return (c.id !== id) ? c : contact;})
};
You can find the contact, avoid mutation by using spread, set new value of show :
case "EDIT_CONTACT":
const { id, show } = action.payload; // Assume id and show are in action.payload
const contact = { ...state.contacts.find(c => c.id === id), show };
return {
...state,
contacts: [...state.contacts.filter(c => c.id !== id), contact]
};
If order matters:
const { id, show } = action.payload;
const contact = { ...state.contacts.find(c => c.id === id), show };
const index = state.contacts.findIndex(c => c.id === id);
return {
...state,
contacts = [ ...state.contacts.slice(0, index), contact, ...state.contacts.slice(index + 1)];
}