React Hooks, copied data keeps changing original source data - reactjs

I'm creating a filtering page. I make a call to the api and then I copy the data so that with each filter requests i'm filtering from the original source. But for some reason when i'm filtering it's modifying my source. So when i filter twice its giving me wrong data. I put flags to make sure my reducer isnt' being called again.
import React, {
useEffect,
useState
} from 'react';
import {
useDispatch,
useSelector
} from "react-redux";
import {
useParams
} from "#reach/router"
import {
fetchSession
} from "../actions/dataActions";
import _ from 'lodash'
import {
useFormik
} from 'formik';
function PageFilter() {
const sessionData = useSelector(state => state.sessionData); //redux
const [filteredData, setFilteredData] = useState([]);
const formik = useFormik({
initialValues: {
date: '',
sessionType: '',
events: ''
},
onSubmit: values => {
filterData(values) //when i hit submit filter data
},
});
function filterData(values) {
let tempFilter = [];
if (values.date != '') {
sessionData.schedule_by_day.map((c, i) => {
if (c.date === values.date) {
tempFilter = [...tempFilter, c];
}
});
} else {
tempFilter = [...sessionData.schedule_by_day];
}
if (values.sessionType != '') {
tempFilter.map((c, i) => {
let tempSessionType = [];
c.session_types.map(s => {
if (parseInt(values.sessionType) == s.id) {
tempSessionType = [...tempSessionType, s]
}
});
tempFilter[i].session_types = [...tempSessionType];
//!!!!!!!
//this is a problem issue, it keeps changing my original data
//so when i call this again it's filtering from a modified SessionData
//!!!!!!!
})
} else {
}
setFilteredData([...tempFilter])
}
useEffect(() => {
dispatch(fetchSession(params.id)); //request data and updates state.sessionData
}, [])
useEffect(() => {
///when sessionData is updated filter the Data based off default form values which displays everything
if (_.isEmpty(sessionData) == false) {
filterData(formik.values)
}
}, [sessionData])

Yes, you are doing a state mutation with tempFilter[i].session_types = [...tempSessionType];. When you update any part of state you must copy it first then update the property.
It is actually rather unclear what you are really trying to do in this handler. You map a bunch of data, but don't return or save the result. It appears as though you meant to use array::forEach to issue a side-effect of pushing elements into an external array. That being said, the following is how you would/should copy the element at the specified index and create a new session_types property object reference so you are not mutating the original reference.
tempFilter[i] = {
...tempFilter[i],
session_types: [...tempSessionType],
}

Related

How to fix invalid hook call error in my airtable extension?

I'm getting this error in my Airtable blocks extension:
Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
I'm unsure as to why. My program uses airtable blocks api to create a dropdown that sets a useState value, before useEffect updates a database live. Both hooks are being used inside the function body, and the function should be a react component, so I don't understand where the error lies. I've seen it can be due to react version conflicts and such as well, but I'm not sure how to confirm whether or not that is the underlying issue.
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import {
Select,
initializeBlock,
SelectSynced,
useBase,
useRecords,
BaseProvider,
useGlobalConfig,
expandRecord,
TablePickerSynced,
ViewPickerSynced,
FieldPickerSynced,
FormField,
Input,
Button,
Box,
Icon,
} from '#airtable/blocks/ui';
import { FieldType } from '#airtable/blocks/models';
const base = useBase();
const table = base.getTable("National Works In Progress");
export default function FilterApp() {
// YOUR CODE GOES HERE
let records = useRecords(table);
var aunumbers_array = [];
const [value, setValue] = useState("");
const queryResult = table.selectRecords({ fields: ["AU Number"] });
records.forEach(function (x) {
if (aunumbers_array.indexOf(x.getCellValueAsString("AU Number"), -1)) {
aunumbers_array.push({ value: x.getCellValueAsString("AU Number"), label: x.getCellValueAsString("AU Number") })
}
});
queryResult.unloadData();
let updates = [];
useEffect(() => {
const fetchData = async () => {
records.forEach(function (x) {
if (x.getCellValueAsString('AU Number') == value) {
updates.push({ id: x.id, fields: { 'Matches Filter': true } });
console.log(value);
}
else if (x.getCellValueAsString('AU Number') !== value && x.getCellValueAsString('Matches Filter') == 'checked') {
updates.push({ id: x.id, fields: { 'Matches Filter': false } });
}
});
while (updates.length) {
await table.updateRecordsAsync(updates.splice(0, 50));
}
}
// call the function
fetchData()
// make sure to catch any error
.catch(console.error);
}, [value])
return (
<div>
<FormField label="Text field">
<Select
options={aunumbers_array}
value={value}
onChange={newValue => setValue(newValue.toString())}
width="320px"
/>
</FormField>
</div>
);
}
The error is because you are calling useBase, a custom hook in an invalid way.
const base = useBase();
This is wrong way of calling a hook as hooks can only be called inside of the body of a function component.
Move the hook call inside FilterApp() functional component.

Passing fetched data to context-Provider in react

I am fetching data from a English language dictionary API and creating a front end in react JS. The code works if I render out all components in the Form.js but I am trying to pass returned data to the wordDataContext to create components separately.
import React, {useEffect, useState , useContext } from 'react';
import {UserContext} from '../context/UserContext';
import {WordContext,WordProvider} from '../context/WordContext';
const getWord = async() => {
if (word !== '') {
setWord(word)
try {
const response = await fetch(`https://mydictionaryapi.appspot.com/?define=${word}`)
const data = await response.json()
iterateObject(data)
setWordData(data) // pass to context
console.log('word data', wordData)
} catch(error) {
let message = "Word NOT Found - Check spellings or connection"
setMeaning(message)
console.log(error)
}
}
}
I am then iterating over the returned dictionary object to extract definitions(meanings), synonyms and example sentence etc.
function iterateObject(data) {
for (var item in data) {
if (typeof(data[item]) == "object"){
if (item === "synonyms"){
var synonyms = data[item]
setSynonyms(synonyms)
}
iterateObject(data[item])
}
else
{
if (item === "definition") {
var meaning = data[item]
setMeaning(meaning)
}
if (item === "example"){
var example = data[item]
setExample(example)
}
}
}
}
I can render out components from iteration but I want to pass the dictionary object ( or iterated items ) to a global wordDataContext to make separate components. Here is the code for my wordDataContext and Provider,
import React, {useState, createContext } from 'react';
export const WordContext = createContext([ [{}], () => {}])
export const WordProvider= (props) => {
const [wordData , setWordData] = useState({ word:'' , meaning:'', synonyms:{} , example:'' })
return(
<WordContext.Provider value={[wordData, setWordData]}>
{props.children}
</WordContext.Provider>
)
}
Unfortunately, the returned object is not getting passed to the WordDataContext I have tried doing so after fetching the data in API call function and also when iterating but only get an empty array

How to reset/clear React Context with a button

I can't clear context even when I'm setting the state to null in the context file. Can anyone tell me what's wrong with my code? This is my code:
import React, { createContext, useState } from 'react';
export const MembersContext = createContext([{}, () => {}]);
export const MembersProvider = ({ children }) => {
const [members, setMembers] = useState(null);
const refreshMembers = async () => {
try {
const data = await request('api/members');
setMembers(data);
} catch (error) {
console.log('ERROR: ', error);
}
};
const clearMembers = () => {
setMembers(null);
console.log('CLEARED MEMBERS IN CONTEXT FILE', members); // not cleared
};
return (
<MembersContext.Provider
value={{
members,
clearMembers,
}}
>
{children}
</MembersContext.Provider>
);
};
Then in my sign out page I have a button to use the clear context function:
import React, { useContext } from 'react';
import {
Button,
} from 'react-native';
import { MembersContext } from '../MembersContext';
const Settings = () => {
const { clearMembers, members } = useContext(MembersContext);
const clearContext = () => {
clearMembers();
console.log('CLEARED MEMBERS?: ', members); // not cleared
//logout()
};
return (
<Button onPress={()=> clearContext()}>Log Out</Button>
);
};
export default Settings;
My console log and screen still shows the data from the previous session.
Let's see both scenarios :-
console.log('CLEARED MEMBERS?: ', members); // not cleared - Here you're not logging the value of members that will get updated but the value of members on which clearContext() closed over i.e. the current state value (before update).
console.log('CLEARED MEMBERS IN CONTEXT FILE', members); // not cleared - This isn't the right way to see if members changed. The state
update is async. Doing a console.log(...) just after updating
members won't work. And I think the reason is same as above. It won't work because the clearMembers() function closes over current value of members.
Each update to members also result's in a new clearMembers()/clearContext() (due to re-render), atleast in this case. That's why these functions can always access the latest state.
To check whether members actually updated, log the value either in the function body of Settings or inside useEffect with members in it's dependency array.

How do I update an array using the useContext hook?

I've set a Context, using createContext, and I want it to update an array that will be used in different components. This array will receive the data fetched from an API (via Axios).
Here is the code:
Context.js
import React, { useState } from "react";
const HeroContext = React.createContext({});
const HeroProvider = props => {
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
const [heroesContext, setHeroesContext] = useState(heroInformation);
return (
<HeroContext.Provider value={heroesContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };
See above that I created the context, but set nothing? Is it right? I've tried setting the same name for the array and function too (heroesContex and feedHeroes, respectively).
Component.js
import React, { useContext, useEffect } from "react";
import { HeroContext } from "../../context/HeroContext";
import defaultSearch from "../../services/api";
const HeroesList = () => {
const context = useContext(HeroContext);
console.log("Just the context", context);
useEffect(() => {
defaultSearch
.get()
.then(response => context.feedHeroes(response.data.data.results))
.then(console.log("Updated heroesContext: ", context.heroesContext));
}, []);
return (
//will return something
)
In the Component.js, I'm importing the defaultSearch, that is a call to the API that fetches the data I want to push to the array.
If you run the code right now, you'll see that it will console the context of one register in the Just the context. I didn't want it... My intention here was the fetch more registers. I have no idea why it is bringing just one register.
Anyway, doing all of this things I did above, it's not populating the array, and hence I can't use the array data in another component.
Does anyone know how to solve this? Where are my errors?
The issue is that you are declaring a piece of state to store an entire context object, but you are then setting that state equal to a single destructured array.
So you're initializing heroesContext to
const heroInformation = {
heroesContext: [],
feedHeroes: arrayFromAPI => {
setHeroesContext(...arrayFromAPI);
console.log();
}
};
But then replacing it with ...arrayFromAPI.
Also, you are not spreading the array properly. You need to spread it into a new array or else it will return the values separately: setHeroesContext([...arrayFromAPI]);
I would do something like this:
const HeroContext = React.createContext({});
const HeroProvider = props => {
const [heroes, setHeroes] = useState([]);
const heroContext = {
heroesContext: heroes,
feedHeroes: arrayFromAPI => {
setHeroes([...arrayFromAPI]);
}
};
return (
<HeroContext.Provider value={heroContext}>
{props.children}
</HeroContext.Provider>
);
};
export { HeroContext, HeroProvider };

React Custom Hook produce warning: Maximum update depth exceeded

I'm new to React Hooks and are taking the first steps... Any help appreciated! I want to re-use logic for sorting and transforming data sets before rendering in charts. So I split it into a Custom hook but get a warning and it seems to be in a re-render loop (slowly counting up)
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
I should have a dependency array and the dependencies only change on button click.. So I don't understand why it goes into a re-render loop...?
CigarettesDetailsContainer receives "raw" data in props and passes transformed data to a child component rendering the chart. It also handles changing dates from the child so I keep that state in here.
The useSingleValueChartData hook transforms the raw data and should re-run when changes to date and time period.
CigarettesDetailsContainer
import React, { FC, useState } from 'react'
import moment from 'moment'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import { CigarettesDetails } from './layout'
import { useSingleValueChartData } from 'hooks/useSingleValueChartData'
import { TimePeriod } from 'models/TimePeriod'
interface Props {
registrations: ApiRegistration[]
}
const initialStart = moment()
.year(2018)
.week(5)
.startOf('isoWeek')
const initialEnd = initialStart.clone().add(1, 'week')
const initialPeriod = TimePeriod.Week
const CigarettesDetailsContainer: FC<Props> = ({ registrations }) => {
const [startDate, setStartDate] = useState(initialStart)
const [endDate, setEndDate] = useState(initialEnd)
const [timePeriod, setTimePeriod] = useState(initialPeriod)
const data = useSingleValueChartData(
registrations,
startDate.toDate(),
endDate.toDate(),
timePeriod
)
const handleTimeChange = (change: number) => {
let newStartDate = startDate.clone()
let newEndDate = endDate.clone()
switch (timePeriod) {
default:
newStartDate.add(change, 'week')
newEndDate.add(change, 'week')
break
}
setStartDate(newStartDate)
setEndDate(newEndDate)
}
return <CigarettesDetails onTimeChange={handleTimeChange} data={data} />
}
export default CigarettesDetailsContainer
useSingleValueChartData
import React, { useEffect, useState } from 'react'
import moment from 'moment'
import { ApiRegistration } from 'models/Api/ApiRegistration'
import { TimePeriod } from 'models/TimePeriod'
import { GroupedChartData, SingleValueChartData } from 'models/ChartData'
import { createWeekdaysList } from 'components/Core/Utils/dateUtils'
export function useSingleValueChartData(
registrations: ApiRegistration[],
startDate: Date,
endDate: Date,
timePeriod: TimePeriod = TimePeriod.Week
) {
const [data, setData] = useState<SingleValueChartData[]>([])
// used for filling chart data set with days without registrations
let missingWeekDays: string[] = []
useEffect(() => {
// which days are missing data
// eslint-disable-next-line react-hooks/exhaustive-deps
missingWeekDays = createWeekdaysList(startDate)
const filteredByDates: ApiRegistration[] = registrations.filter(reg =>
moment(reg.date).isBetween(startDate, endDate)
)
const filteredByDirtyValues = filteredByDates.filter(reg => reg.value && reg.value > -1)
const grouped: SingleValueChartData[] = Object.values(
filteredByDirtyValues.reduce(groupByWeekDay, {} as GroupedChartData<
SingleValueChartData
>)
)
const filled: SingleValueChartData[] = grouped.concat(fillInMissingDays())
const sorted: SingleValueChartData[] = filled.sort(
(a: SingleValueChartData, b: SingleValueChartData) =>
new Date(a.date).getTime() - new Date(b.date).getTime()
)
setData(sorted)
}, [startDate, timePeriod])
function groupByWeekDay(
acc: GroupedChartData<SingleValueChartData>,
{ date: dateStr, value }: { date: string; value?: number }
): GroupedChartData<SingleValueChartData> {
const date: string = moment(dateStr).format('YYYY-MM-DD')
acc[date] = acc[date] || {
value: 0,
}
acc[date] = {
date,
value: value ? acc[date].value + value : acc[date].value,
}
// remove day from list of missing week days
const rest = missingWeekDays.filter(d => d !== date)
missingWeekDays = rest
return acc
}
function fillInMissingDays(): SingleValueChartData[] {
return missingWeekDays.map(date => {
return {
value: 0,
date,
}
})
}
return data
}
In the custom hook, though you want to run the effect only on change of startDate or timePeriod, at present the effect is run everytime.
This is because how startDate and endDate params are being passed to custom hook.
const data = useSingleValueChartData(
registrations,
startDate.toDate(),
endDate.toDate(),
timePeriod
)
.toDate returns new date object.
So every time new date object is being passed to custom hook.
To correct this, pass the startDate and endDate directly (i.e. without toDate) to custom hook and manage the moment to date conversion in the custom hook.

Resources