I've got the following data structure stored in a useState hook.
const [data, setData] = useState([
{
id: uniqid(),
title: "",
content:[
id: uniqid(),
title: "",
]
},
{
id: uniqid(),
title: "",
content:[
id: uniqid(),
title: "",
],
}
])
I've got a button where the user can add something to the content array, and I'm calling handleReport as below -
const handleAddReport = uniqueID =>{
const object = {
id: uniqid(),
title:"",
}
const formData = [...data];
formData.map(section=>{
section.content.map(report=>{
if(report.id === uniqueID){
section.content.push(object);
};
});
});
setForm(formData);
}
However, this isn't changing the form data at all. I'm not exactly sure how I could get it to work, any help would be appreciated! Thanks
you are not returning anything from the map.
const handleAddReport = uniqueID =>{
const object = {
id: uniqid(),
title:"",
}
const formData = [...data];
const newData = formData.map(section=> {
if(section.id === uniqueID){
section.content.push(object);
}
return section;
});
setForm(newData);
}
But instead of comparing the unqiueId another approach would be to pass the index of your data array. So that we can avoid map .
const handleAddReport = dataIndex =>{
const object = {
id: uniqid(),
title:"",
}
// deep clone the data
const clonedData = JSON.parse(JSON.stringify(data));
// since the data is cloned you can mutate it directly
clonedData[dataIndex].content.push(object);
setForm(clonedData)
}
Try this way
const onAddReport = (uniqid: number) => {
const obj = {
id: 3,
title: ''
};
const formData = [...data];
const mappedData = formData.map((data) => data.id === uniqid ? ({ ...data, content: [...data.content, obj] }) : data)
setData(mappedData);
}
Related
I have addText() that runs on click event
const [list, setList] = useState([])
const [value, setValue] = useState("")
useEffect(() => {
getObjectItem("tasks")
.then(t => setList(t.item))
.catch(e => { console.log(e) })
}), []
// A function that add data to the list array
function addText(text) {
console.log(list);
if (value !== "") {
setList(prev =>
[...prev,
{ text: text, isSelected: false }] // Adding a JS Object
)
setObjectItem("tasks", list);
setValue("")
} else {
alert("Please type in something!")
}
}
Output from console.log(list):
Array [
Object {
"isSelected": true,
"text": "Test",
}
]
getObjectItem("tasks") function:
const getObjectItem = async (name) => {
try {
const jsonItem = await AsyncStorage.getItem(name)
const item = JSON.parse(jsonItem)
return {
status: 'success',
name: name,
item: item
}
} catch (err) {
return {
status: 'error',
name: name,
error: err
}
}
}
Why can't I add values to the existing list array with setList() in addText() function?
Setting state is asynchronous.
In addText you write:
setObjectItem("task", list)
which will set the value in AsyncStorage to whatever list was, not what it will be after the state has been updated. The easiest solution is to create the new array then set it to state and AsyncStorage.
Try to put
.then(t => setList([t.item]))
instead of what you wrote
This is my test code snippet but it throws an exception, TypeError: componentInstance.loadLoanApplication is not a function :
it('should render the SubmittedLoan', () => {
const loanData = {
data: {
id: 1,
};
const div = document.createElement('div');
const wrapper = mount(
<AppProviders>
<MemoryRouter initialEntries={['/review/153']}>
<SubmittedLoan
match={{ params: { loanId: 1, step: 1 } }}
history={{
location: { state: { from: 'register' } },
push() {},
}}
/>
</MemoryRouter>
</AppProviders>,
div,
);
const componentInstance = wrapper
.find(SubmittedLoan)
.children()
.first()
.children()
.first()
.instance();
const loanApplication = {
id: 1,
steps_data: [
{ slug: 'step_1', title: 'Step 1' },
{ slug: 'step_2', title: 'Step 2' },
],
status: ApiCaptiq.STATUS_SUBMITTED,
};
expect(wrapper.find(SubmittedLoan).length).toBe(1);
componentInstance.loadLoanApplication(1, 1);
componentInstance.onLoadLoanApplication(loanData);
componentInstance.onLoadFail();
componentInstance.setState({
formData: [{ item: 'value' }, { item2: 'value2' }],
activeStep: 1,
loanApplication,
});
componentInstance.handleSnackbarClose(new Event('click'), '');
componentInstance.setState({ activeStep: 3 });
});
Then my Component which uses memo is as follows :
export const SubmittedLoan = memo(() => {
const [loanApplication, setLoanApplication] = useState<LoanApplication | null>(null);
const [message, setMessage] = useState({
message: '',
open: false,
messageType: '',
});
const authContext = useContext(AuthContext);
const customerContext = useCustomerData();
const params = useParams();
const history = useHistory();
const classes = useStyles();
const { loanId } = params;
const onLoadFail = useCallback(() => {
setMessage({
message: 'Die verfügbaren Darlehensarten können nicht aufgelistet werden',
open: true,
messageType: 'error',
});
}, []);
const onLoadLoanApplication = useCallback(
(response: AxiosResponse) => {
setTemplateSettings(response, authContext);
if (
response.data.status === ApiCaptiq.STATUS_STARTING ||
response.data.status === ApiCaptiq.STATUS_IN_PROGRESS ||
response.data.status === ApiCaptiq.STATUS_PRE_WAITING
) {
history.push(`/view/${loanId}`);
} else {
setLoanApplication(response.data);
}
},
[loanId, authContext, history],
);
const loadLoanApplication = useCallback(
async (loan_id: number) => {
try {
const response = await request.get(`${ApiCaptiq.LOAN_APPLICATION_URL}${loan_id}/`);
const { fetchCustomerProfile } = customerContext;
await fetchCustomerProfile(response.data.customer_profile_id);
onLoadLoanApplication(response);
} catch (err) {
onLoadFail();
}
},
[customerContext, onLoadLoanApplication, onLoadFail],
);
...
What could be the possible reason for this
The functions you are defining inside the component, are not just available on the component instance. In fact, there is not way to call them. You can test only by mocking the fetch calls they are doing.
If you really need callable functions in your component (you should try to avoid these..), you could use this: https://reactjs.org/docs/hooks-reference.html#useimperativehandle
Perhaps better would be to extract this data loading logic elsewhere and test it separately.
So I have a declared constants variable object. i.e:
const objData = {
project: {
name: '',
age: 0,
subProject: {
name: '',
age: 0
}
}
}
And the function that returns the objData:
const dataSchema = () => {
return objData;
}
The flow is, I want to insert data into my table/collection in my database. So that's why I'm using constants to declare schema, and then I just insert with the returned dataSchema.
This is my func to set value into dataSchema:
const mappingData1 = (data) => {
try {
const dataScheme = Object.assign({}, dataSchema())
dataScheme.project.name = data.name;
dataScheme.project.age = data.age;
return dataScheme;
}catch(err){
return err; //return wrapper error here
}
}
const mappingData2 = (data) => {
try {
const dataScheme = Object.assign({}, dataSchema())
dataScheme.project.subProject.name = data.name;
dataScheme.project.subProject.age = data.age;
return dataScheme;
}catch(err){
return err; //return wrapper error here
}
}
Call mappingData schema func, and insert data into db:
//this bellow statement will be called in insertProjectData func, and called in the first time
const data = { name: 'Someone', age: 7 }
const mappedData = await mappingData1(data)
const result = await this.command.insertData(mappedData)
===================================================================================
//this bellow statement will be called in insertSubProjectData func, and called after insertProjectData func
const data = { name: 'Someone', age: 7 }
const mappedData = await mappingData2(data)
const result = await this.command.insertData(mappedData)
The problem is, when the service is running, and insertSubProjectData func is called after insertProjectData func, why project.name and project.age is setted too with the value from insertProjectData func? even though i didn't set project.name and project.age in insertSubProjectData? It's like the original data (objData) is has been modified. I already use Object.assign too, to create a new object
Any idea please? thank you!
You can guarantee a new object by changing your implementation from this:
const objData = {
project: {
name: '',
age: 0,
subProject: {
name: '',
age: 0
}
}
}
// And the function that returns the objData:
const dataSchema = () => {
return objData;
}
To this:
const dataSchema = () => ({
project: {
name: '',
age: 0,
subProject: {
name: '',
age: 0
}
}
})
Then every time you'll have a real new Object. No more issues
I have a store setup that has multiple arrays
I'm trying to search all arrays at once, via a textfield.
I can get this done, by calling a selector function on keyup, that filters the 4 arrays and pushes to a new array.
I've thought about merging all the arrays to one array before filtering, but I want to keep the results separate, as they are going to be displayed in categories.
Just trying to see if I can streamline the performance at all and if there's a more concise way of doing this, in case I need to do something similar with larger arrays.
my textField function:
this.renderer.listen(this.serachField.nativeElement, 'keyup', (event) => {
if (this.serachField.nativeElement.value.length < 3) { return; }
this.store.pipe(select(search(this.serachField.nativeElement.value)),take(1))
.subscribe(data => {
console.log(data);
});
})
The selector function:
export const search = (searchString: string): any => {
return createSelector(
appState,
(state: AppState) => {
let arr: any = [];
let s = searchString.toUpperCase();
const comics = state.comics.results.filter((item: Comic) => {
let title = item.title.toUpperCase();
console.log(title);
return title.includes(s);
});
const music = state.music.items.filter((item: Album) => {
let title = item.name.toUpperCase();
console.log(title);
return title.includes(s);
});
const movies = state.movies.results.filter((item: Movie) => {
let title = item.title.toUpperCase();
console.log(title);
return title.includes(s);
});
const games = state.games.filter((item: Game) => {
let title = item.title.toUpperCase();
console.log(title);
return title.includes(s);
});
arr.push(comics, music, movies, games);
return arr;
}
);
};
EDIT: After #GustavMH correct answer I had to slightly change the code to be a little more dynamic in terms of the array naming as follows
export const search = (searchString: string): any => {
return createSelector(
appState,
(state: any) => {
let s = searchString.toUpperCase();
const keys = [{state: "comics", item: "title", array: 'results'},
{state: "music", item: "name", array: 'items'},
{state: "movies", item: "title", array: 'results'},
{state: "games", item: "title", array: ''}]
return keys.map((key) => {
let arr = key.array ? state[key.state][key.array] : state[key.state];
return arr.filter((item: any) => {
const title = item[key.item].toUpperCase();
console.log(item);
return title.includes(s);
})})
}
);
};
This should implement the selector function with less code and make it more adaptable to kinds of data, if needed you can specify a more precise type in the filter function.
export const search = (searchString: string): any => {
return createSelector(
appState,
(state: AppState) => {
let s = searchString.toUpperCase();
const keys = [{state: "comics", item: "title"},
{state: "music", item: "name"},
{state: "movies", item: "title"},
{state: "games", item: "title"}]
return keys.map(key => state[key.state].results.filter((item: any) => {
const title = item[key.item].toUpperCase();
console.log(title);
return title.includes(s);
}))
}
);
};
This is my initial data
const data = [
{ id: '1', name: '1' },
{ id: '2', name: '1' },
{ id: '3', name: '2' },
]
I want to loop over and:
Where it has name 1 add that object to stateOne
Where it has name 2 add that object to stateTwo
End goal both states needs to have Array of Objects inside:
stateOne needs to look like
[
{ id: '1', name: '1' },
{ id: '2', name: '1' }
]
stateTwo needs to look like
[
{ id: '3', name: '2' },
]
This is what i've tried:
const data = [
{ id: '1', name: '1' },
{ id: '2', name: '1' },
{ id: '3', name: '2' },
]
const Testing = () => {
const [stateOne, setStateOne] = useState([])
const [stateTwo, setStateTwo] = useState([])
useEffect(() => {
data.forEach((e) => {
if (e.name === '1') {
console.log('e', e)
setStateOne((prevSate) => ({ ...prevSate, e }))
}
// if (e.name === '2') {
// setStateTwo(e)
// }
})
}, [])
console.log('stateOne', stateOne)
}
I'd prefer sending data as a prop to that component
You can achieve what you need by
const data = [
{ id: '1', name: '1' },
{ id: '2', name: '1' },
{ id: '3', name: '2' },
]
export default function Testing() {
const [stateOne, setStateOne] = useState([])
const [stateTwo, setStateTwo] = useState([])
useEffect(() => {
setStateOne(data.filter(e => e.name === "1"))
setStateTwo(data.filter(e => e.name === "2"))
console.log('stateOne', stateOne)
console.log('stateTwo', stateTwo)
}, [])
}
setState functions as an assignment. Like you would normally assign a variable. That means if you want to add something to an array, you need to include that array in the assignment.
Something like this:
if (e.name === '1') {
console.log('e', e)
setStateOne([...stateOne, e])
}
if (e.name === '2') {
setStateTwo([...stateTwo, e])
}
If you don't want to use filter twice for whatever reason, You can create temporary array for each one and manipulate them then update each state respectively like so:
const [stateOne, setStateOne] = useState([]);
const [stateTwo, setStateTwo] = useState([]);
useEffect(() => {
const tempArr1 = [];
const tempArr2 = [];
data.forEach((item) => {
if (item.name === "1") {
tempArr1.push(item);
} else if (item.name === "2") {
tempArr2.push(item);
}
});
setStateOne(tempArr1);
setStateTwo(tempArr2);
}, []);
console.log(stateOne);
console.log(stateTwo);
The problem with what you're doing is you're updating the state each time you find a match which will cause a lot of unnecessary re-renders.
You've said that data comes from some API you're querying. If so, filter the data once you get it. You can do that in a couple of ways.
With two calls to filter:
const Testing = () => {
const [stateOne, setStateOne] = useState([]);
const [stateTwo, setStateTwo] = useState([]);
useEffect(() => {
let cancelled = false;
getTheData(data => {
if (cancelled) {
return;
}
setStateOne(data.filter(({name}) => name === "1"));
setStateTwo(data.filter(({name}) => name === "2"));
};
return () => {
// So we don't try to set setate on an unmounted component
cancelled = true;
};
}, []);
// ...use `dataOne` and `dataTwo` here...
};
Or if you don't want to make two passes through the data, a single loop:
const Testing = () => {
const [stateOne, setStateOne] = useState([]);
const [stateTwo, setStateTwo] = useState([]);
useEffect(() => {
let cancelled = false;
getTheData(data => {
if (cancelled) {
return;
}
const stateOne = [];
const stateTwo = [];
for (const entry of data) {
switch (entry.name) {
case "1":
stateOne.push(entry);
break;
case "2": // or default if you want all non-1s in `stateTwo`
stateTwo.push(entry);
break;
}
}
setStateOne(stateOne);
setStateTwo(stateTwo);
};
return () => {
// So we don't try to set setate on an unmounted component
cancelled = true;
};
}, []);
// ...use `dataOne` and `dataTwo` here...
};
const data = [
{ id: "1", name: "1" },
{ id: "2", name: "1" },
{ id: "3", name: "2" }
];
const App = () => {
const newdata = useState(data);
const [stateOne, setStateOne] = useState([]);
const [stateTwo, setStateTwo] = useState([]);
const Filter = () => {
let listOne = [];
let listTwo = [];
newdata[0].map((it) => {
if (it.name === "1"){
listOne.push(it);
}
else if(it.name === "2"){
listTwo.push(it)
}
});
setStateOne(listOne);
setStateTwo(listTwo);
};
useEffect(() => {
Filter();
}, []);
console.log("stateOne", stateOne)
console.log("stateTwo", stateTwo)
return (
// your code
)
};