React Hooks reducer causing memory leak - reactjs

I have a React component that calls a reducer on initialisation to populate an array with as many blank records as a number specified in a firebase db. If the firebase db field myArray length is 5, the component will initialise and call the reducer to create 5 blank records in an array in the context:
import React, { useState, useEffect, useContext } from 'react';
import { useHistory, useLocation } from 'react-router';
import ArrayCard from '../../../containers/ArrayCard';
import firebase from '../../../Utils/firebase';
import MyContext from '../../../Utils/contexts/MyContext ';
import { initialiseMyArray, changeArray} from '../../../Utils/reducers/MyActions';
function MyComponent() {
const { push } = useHistory();
const location = useLocation();
const context = useContext(MyContext);
const dispatch = context.dispatch;
const session = location.state.currentSession;
const sessionRef = firebase.firestore().collection("sessions").doc(session);
const [ loaded, setLoaded ] = useState(false);
const [ myArray, setMyArray] = useState([]);
const [ myArrayCurrentIndex, setMyArrayCurrentIndex ] = useState();
const getSessionData = () => {
sessionRef.get().then(function(doc) {
if (doc.exists) {
const myArrayLength= parseInt(doc.data().myArrayLength);
dispatch({ type: initialisePensionsFromFirebase, myArrayLength: myArrayLength});
setMyArrayCurrentIndex (0);
setLoaded(true);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
}
useEffect(() => {
if (sessionRef && !loaded && myArray.length < 1) {
getSessionData();
}
if (context) {
console.log("CONTEXT ON PAGE: ", context)
}
}, [loaded, currentIndex])
The index currentIndex of the newly-created myArray in the context is used to populate a component inside this component:
return (
<innerComponent
handleAmendSomeproperty={(itemIndex, field, value) => handleAmendSomeproperty(itemIndex, field, value)}
itemIndex={currentIndex}
someproperty={context.state.someitem[currentIndex].someproperty}
></innerComponent>
)
I want to be able to amend the someproperty field in myArray[currentIndex], but my dispatch causes endless rerenders.
const handleAmendSomeproperty= (itemIndex, field, value) => {
dispatch({ type: changeItem, itemIndex: itemIndex});
}
My reducer switch cases are like so:
case initialiseMyArray:
console.log("SRSTATE: ", state);
let _initialArray = []
for (let i=0; i<action.myArrayLength; i++) {
_initialArray .push(
{
itemIndex: i,
someproperty: "" }
)
}
return {
...state,
someproperty: [..._initialArray ]
};
case changeArray:
// I want to leave the state untouched at this stage until I stop infinite rerenders
return {
...state
};
What is happening to cause infinite rerenders? How come I can't amend someproperty, but initialising the new array in the state works fine?
innerComponent has lots of versions of this:
<div>
<label>{label}</label>
<div onClick={() => handleAmendSomeproperty(itemIndex, "fieldName", "fieldValue")}>
{someproperty === "booleanValue" ? filled : empty}
</div>
</div>
and lots like this:
<input
type="text"
placeholder=""
value={somepropertyvalue}
onChange={(event) => handleAmendSomeproperty(itemIndex, "fieldName", event.target.value)}
/>

Related

Why is my Saved State Overwritten on Refresh in this Simple React Example?

I am following this simple tutorial and can't get my saved state to work.
I can see in the comments that other users are having to work around this issue.
import React, { useState, useRef, useEffect } from "react"
import TodoList from "./TodoList"
import { v4 as uuidv4 } from 'uuid';
const LOCAL_STORAGE_KEY = 'todosApp.todos'
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
setTodos([...todos, { id:uuidv4(), name:name, complete:false }])
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}
export default App;
Here's the output:
useEffect[]
App.js:15 set todos to: [{"id":"77fe1e9e-91aa-4a34-9bfb-b1842ea5518d","name":"asfd","complete":false},{"id":"8dabea66-4ed9-4f10-9003-af1b34b4558a","name":"asfd","complete":false},{"id":"6d4e9350-11cd-4ace-8766-485e1f8817ad","name":"asfd","complete":false}]
App.js:23 useEffect[todos]: []
App.js:12 useEffect[]
App.js:15 set todos to: []
App.js:23 useEffect[todos]: []
App.js:23 useEffect[todos]: []
So it seems like the state is asynchronously initialised after loading the state for some reason.
Here is my workaround:
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
So I have a workaround, but why is this necessary? I can't wrap my head around how this can be intended React functionality.
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
useEffect is always triggered initially, even though it has dependencies. In your case with the above snippet, it will be triggered twice:
Initial loading (like [] - no dependencies)
Updated todos state
So that's why it set empty data to localStorage because of initial loading without empty todos (if you don't have the condition todos.length != 0)
Your above snippet with the condition todos.length != 0 is reasonable, but it won't work for delete-all cases.
If you don't use any server-side rendering frameworks, you can set a default value for todos state
const [todos, setTodos] = useState(localStorage.getItem(LOCAL_STORAGE_KEY))
With this change, you can update useEffect like below
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
If you use a server-side rendering framework like NextJS, you can try to update localStorage directly on events (toggleTodo and handleAddTodo) instead of useEffect.
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newTodos))
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
const updatedTodos = [...todos, { id:uuidv4(), name:name, complete:false }]
setTodos(updatedTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedTodos))
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}

Context API values are being reset too late in the useEffect of the hook

I have a FilterContext provider and a hook useFilter in filtersContext.js:
import React, { useState, useEffect, useCallback } from 'react'
const FiltersContext = React.createContext({})
function FiltersProvider({ children }) {
const [filters, setFilters] = useState({})
return (
<FiltersContext.Provider
value={{
filters,
setFilters,
}}
>
{children}
</FiltersContext.Provider>
)
}
function useFilters(setPage) {
const context = React.useContext(FiltersContext)
if (context === undefined) {
throw new Error('useFilters must be used within a FiltersProvider')
}
const {
filters,
setFilters
} = context
useEffect(() => {
return () => {
console.log('reset the filters to an empty object')
setFilters({})
}
}, [setFilters])
{... do some additional stuff with filters if needed... not relevant }
return {
...context,
filtersForQuery: {
...filters
}
}
}
export { FiltersProvider, useFilters }
The App.js utilises the Provider as:
import React from 'react'
import { FiltersProvider } from '../filtersContext'
const App = React.memo(
({ children }) => {
...
...
return (
...
<FiltersProvider>
<RightSide flex={1} flexDirection={'column'}>
<Box flex={1}>
{children}
</Box>
</RightSide>
</FiltersProvider>
...
)
}
)
export default App
that is said, everything within FiltersProvider becomes the context of filters.
Now comes the problem description: I have selected on one page (Page1) the filter, but when I have to switch to another page (Page2), I need to flush the filters. This is done in the useFilters hook in the unmount using return in useEffect.
The problem is in the new page (Page2), during the first render I'm still getting the old values of filters, and than the GraphQL request is sent just after that. Afterwards the unmount of the hook happens and the second render of the new page (Page2) happens with set to empty object filters.
If anyone had a similar problem and had solved it?
first Page1.js:
const Page1 = () => {
....
const { filtersForQuery } = useFilters()
const { loading, error, data } = useQuery(GET_THINGS, {
variables: {
filter: filtersForQuery
}
})
....
}
second Page2.js:
const Page2 = () => {
....
const { filtersForQuery } = useFilters()
console.log('page 2')
const { loading, error, data } = useQuery(GET_THINGS, {
variables: {
filter: filtersForQuery
}
})
....
}
Printout after clicking from page 1 to page 2:
1. filters {isActive: {id: true}}
2. filters {isActive: {id: true}}
3. page 2
4. reset the filters to an empty object
5. 2 reset the filters to an empty object
6. filters {}
7. page 2
As I mentioned in the comment it might be related to the cache which I would assume you are using something like GraphQL Apollo. It has an option to disable cache for queries:
fetchPolicy: "no-cache",
By the way you can also do that reset process within the Page Two component if you want to:
const PageTwo = () => {
const context = useFilters();
useEffect(() => {
context.setFilters({});
}, [context]);
For those in struggle:
import React, { useState, useEffect, useCallback, **useRef** } from 'react'
const FiltersContext = React.createContext({})
function FiltersProvider({ children }) {
const [filters, setFilters] = useState({})
return (
<FiltersContext.Provider
value={{
filters,
setFilters,
}}
>
{children}
</FiltersContext.Provider>
)
}
function useFilters(setPage) {
const isInitialRender = useRef(true)
const context = React.useContext(FiltersContext)
if (context === undefined) {
throw new Error('useFilters must be used within a FiltersProvider')
}
const {
filters,
setFilters
} = context
useEffect(() => {
**isInitialRender.current = false**
return () => {
console.log('reset the filters to an empty object')
setFilters({})
}
}, [setFilters])
{... do some additional stuff with filters if needed... not relevant }
return {
...context,
filtersForQuery: { // <---- here the filtersForQuery is another variable than just filters. This I have omitted in the question. I will modify it.
**...(isInitialRender.current ? {} : filters)**
}
}
}
export { FiltersProvider, useFilters }
What is done here: set the useRef bool varialbe and set it to true, as long as it is true return always an empty object, as the first render happens and/or the setFilters function updates, set the isInitialRender.current to false. such that we return updated (not empty) filter object with the hook.

UseEffect doesn't trigger on second change, but trigger twice on launch.(React hooks and apollo-graphql hooks)

useEffect doesn't trigger on second change, but triggers twice on launch (React hooks and apollo-graphql hooks).
In console.logs I described when the changes are triggered and when not.
I don't have any more clue to add.
Here's my page (Next.js pages)
import React, { useEffect, useState } from 'react'
import Calendar from '../components/Calendar';
import { useHallsQuery } from '../generated/graphql';
const schedule = () => {
const { data, loading, error, refetch:refetchSessions } = useHallsQuery();
const [sessions, setSessions] = useState([] as any);
const [owners, setOwners] = useState([] as any);
useEffect(() => {
console.log('On launch trigger twice, on first change trigger once, on second doesn't trigger and later trigger correctly, but it's one change delay');
if(loading === false && data){
const colors: any = [
'#FF3333',
'#3333FF',
'#FFFF33',
'#33FF33',
'#33FFFF',
'#9933FF',
'#FF9933',
'#FF33FF',
'#FF3399',
'#A0A0A0'
];
let pushSessions:any = [];
let owners:any = [];
data?.halls?.map(({sessions, ...o}, index) =>{
owners.push({id:o.id,
text:o.name,
color: colors[index%10]});
sessions.map((session:any) => {
pushSessions.push({...session,
ownerId: o.id});
})
})
setSessions(pushSessions);
setOwners(owners);
}
}, [loading, data])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
<Calendar sessions={sessions} owners={owners} refetchSessions={refetchSessions} />
</div>
)
}
export default schedule
and my component part where I get props and trigger refetchSessions.
const Calendar = (props: any) => {
let { sessions, owners, refetchSessions } = props;
const [moveSession] = useMoveSessionMutation();
...
const commitChanges = ({ added, changed, deleted }: any) => {
if (added) {
//
}
if (changed) {
console.log('trigger on all changes! Correct')
const id = Object.keys(changed)[0];
moveSession({variables:{
input: {
id: parseInt(id),
...changed[id]
}
}, refetchQueries: refetchSessions
})
}
if (deleted !== undefined) {
//
}
};
return (
// Some material-ui components and #devexpress/dx-react-scheduler-material-ui components in which commitChanges function is handled
)
export default Calendar;
Hook was generated with graphql-codegen(generated/graphql.tsx):
export function useHallsQuery(baseOptions?: Apollo.QueryHookOptions<HallsQuery, HallsQueryVariables>) {
return Apollo.useQuery<HallsQuery, HallsQueryVariables>(HallsDocument, baseOptions);
}
export function useHallsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HallsQuery, HallsQueryVariables>) {
return Apollo.useLazyQuery<HallsQuery, HallsQueryVariables>(HallsDocument, baseOptions);
}
export type HallsQueryHookResult = ReturnType<typeof useHallsQuery>;
and here's the schema(graphql/hall.graphql):
query Halls {
halls {
id
name
sessions{
id
title
startDate
endDate
}
}
}

Preventing of incorrect state with React hooks

I have component
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
const data = {
"1": ["1a", "1b"],
"2": ["2a"]
};
const slugs = {
"1a": { text: "Lorem" },
"1b": { text: "ipsum" },
"2a": { text: "..." }
};
const ExamplePage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [index, setIndex] = useState(0);
useEffect(() => {
setIndex(0);
}, [id]);
console.log("state", {
id,
index
});
const slug = data[id][index];
const text = slugs[slug].text;
function onPrev(): void {
if (index <= 0) {
return;
}
setIndex((index) => index - 1);
}
function onNext(): void {
if (index >= data[id].length - 1) {
return;
}
setIndex((index) => index + 1);
}
return (
<div>
<button onClick={onPrev}>Prev</button>
<span>{text}</span>
<button onClick={onNext}>Next</button>
</div>
);
};
export default ExamplePage;
And Route for this:
<Route path="/:id" component={ExamplePage} />
Live version: https://codesandbox.io/s/react-hooks-ewl4d
There is a bug with this code when:
User is on /1 url
User clicks button "Next"
User clicks link to /2 url
In this case id will be "2", index will be 1, but there isn't data["2"][1].
As you can see useEffect don't help in this case because useEffect don't stop current function call.
My question is: How I can ensure that this state will be always correct?
I know that I can write const text = slugs[slug]?.text; and this solve my bug, but still, in one moment, component have incorrect state. I wondering if is a way to prevent this incorrect state.
In React class component this problem can be solved by getDerivedStateFromProps - You can see this live on https://codesandbox.io/s/react-hooks-solve-in-react-component-xo43g
The useEffect will run async so you are trying to set the slug and text before the index has updated.
You can put the slug and text into state and then use another useEffect to update them when the index or id changes:
const { id } = useParams();
const [index, setIndex] = useState(0);
const [slug, setSlug] = useState();
const [text, setText] = useState();
useEffect(() => {
setIndex(0);
}, [id]);
useEffect(() => {
const newSlug = data[id][index];
if (!newSlug) return; // If id changes but index has not updated yet
setSlug(newSlug);
setText(slugs[newSlug].text);
}, [id, index]);

Apollo query not triggering useEffect

I need a refetch on an Apollo query to trigger useEffect. I'm implementing a filter bar that filters by a date period and a text search. My search submit button triggers the refetch on the query, and the data returned is a dependency of my useEffect. When the data is returned, if the search fields are populated, the filter should run. This works the first time I run a search, but when I reset with my reset function, the reset is triggered and the query gets 200 in Chrome and shows the data in Chrome dev tools, but useEffect is not triggered. Why is this?
//React
import React, {useState, useContext, useEffect, Fragment } from 'react'
//extra React libraries
//external libraries
import { useQuery, useMutation } from '#apollo/react-hooks'
//components
import AccountsGrid from '../../containers/AccountsGrid/AccountsGrid'
import SpinnerLoader from '../../components/SpinnerLoader/SpinnerLoader'
//styles
import styles from "./accounts.module.scss";
//contexts
import UserContext from '../../containers/contexts/UserContext/UserContext'
import AccountContext from '../../containers/contexts/AccountContext/AccountContext'
//constants
import {
GET_PARENT_ACCOUNT,
SET_ACCOUNTS,
SET_IS_ACTIVE,
SET_ACCOUNTS_LIMIT,
SET_ACCOUNTS_OFFSET,
SET_ACCOUNTS_TOTAL,
SET_ACCOUNTS_START_DATE,
SET_ACCOUNTS_END_DATE,
} from '../../containers/contexts/AccountContext/accountActionTypes'
import {
GET_ACCOUNT_USERS,
GET_ACCOUNTS,
TOTAL_ACCOUNTS,
} from '../../utils/constants/queries/accountQueries'
import {
UPDATE_ACCOUNT
} from '../../utils/constants/mutations/accountMutations'
import moment from 'moment';
function Accounts(props) {
const userContext = useContext(UserContext)
const accountContext = useContext(AccountContext)
const parentAccountID = userContext.userState.accountId
const [parentAccountIDs, setParentAccountIDs] = useState(null)
const [sortByField, setSortByField] = React.useState(null);
const [sortByType, setSortByType] = React.useState(null);
const [startDate, setStartDate] = React.useState(null);
const [endDate, setEndDate] = React.useState(null);
const [datesValid, setDatesValid] = React.useState(true);
const [searchText, setSearchText] = React.useState("");
const {
loading: loadingAccountUsers,
error: errorAccountUsers,
data: dataAccountUsers,
refetch: refetchAccountUsers,
} = useQuery(GET_ACCOUNT_USERS, {
variables: {
accountId: parentAccountID
}
})
const {
loading: loadingAccounts,
error: errorAccounts,
data: dataAccounts,
refetch: refetchAccounts
} = useQuery(GET_ACCOUNTS, {
variables: {
parentIds: parentAccountIDs,
offset: accountContext.accountState.data.accountsOffset,
limit: accountContext.accountState.data.accountsLimit
}
})
const {
loading: loadingAccountsTotal,
error: errorAccountsTotal,
data: dataAccountsTotal,
refetch: refetchAccountsTotal
} = useQuery(TOTAL_ACCOUNTS)
const [
updateAccount,
{ loading, error, data: updateAccountData }
] = useMutation(UPDATE_ACCOUNT);
const setParentIDsHandler = (id) => {
setParentAccountIDs(String(id))
}
const setOffset = (offset, limit) => {
accountContext.accountDispatch({
type: SET_ACCOUNTS_OFFSET,
payload: {
offset: offset
}
})
accountContext.accountDispatch({
type: SET_ACCOUNTS_LIMIT,
payload: {
limit: limit
}
})
}
const deactivateUser = row => {
updateAccount({
variables: {
account: {
id: row.id,
isActive: !row.isActive
}
}
})
accountContext.accountDispatch({type: SET_IS_ACTIVE, payload: row.id})
}
const handleRequestSort = (sortType, sortBy) => {
sortRow(sortBy)
setSortByType(sortType)
}
const sortRow = (sortBy) => {
switch(sortBy) {
case 'Contact':
setSortByField('email')
break
case 'First Name':
setSortByField('firstName')
break
case 'Last Name':
setSortByField('lastName')
break
case 'Join Date':
setSortByField('dateJoined')
break
}
}
const setDates = (dates) => {
console.log("SETTING DATES", dates)
if ("startDate" === Object.keys(dates)[0]) {
setStartDate(dates.startDate)
} else {
setEndDate(dates.endDate)
}
}
const checkDatesValid = () => {
if (startDate && endDate) {
if (startDate <= endDate) {
return setDatesValid(true)
}
}
return setDatesValid(false)
}
const clearDates = () => {
console.log("CLEARING")
setDatesValid(true)
setStartDate(null)
setEndDate(null)
setSearchText(null)
accountContext.accountDispatch({type: SET_ACCOUNTS_START_DATE, payload: {startDate: null}})
accountContext.accountDispatch({type: SET_ACCOUNTS_END_DATE, payload: {endDate: null}})
return setDatesValid(true)
}
const searchByChars = (text) => {
console.log("SEARCH TEXT", text)
setSearchText(text)
resetAccounts()
}
const resetAccounts = () => {
console.log("RESET ACCOUNTS TRIGGERED")
refetchAccounts({
variables: {
parentIds: parentAccountIDs,
offset: accountContext.accountState.data.accountsOffset,
limit: accountContext.accountState.data.accountsLimit
}
})
console.log("LOADING", loadingAccounts)
console.log("ERROR", errorAccounts)
}
const filterText = (textRows) => {
let newTextRows = []
for (let row of textRows) {
Object.entries(row['users'][0]).forEach(([key, val]) => {
if (String(val).includes(String(searchText))) {
if (!(newTextRows.includes(row))) {
newTextRows.push(row)
}
}
});
}
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: {accounts: newTextRows}})
}
const filterDates = (dateRows) => {
let newDateRows = []
for (let row of dateRows) {
if (datesValid) {
const _date = moment(row.users[0]['dateJoined'])
const sdate = moment(startDate)
const edate = moment(endDate)
if (_date.isBetween(sdate, edate, null, '[]')) {
if (!(newDateRows.includes(row))) {
newDateRows.push(row)
}
}
}
}
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: {accounts: newDateRows}})
}
useEffect(() => {
if (sortByField && sortByType) {
const newRows = accountContext.accountState.data.accounts.sort((a, b) => {
const compareA = a.users[0][sortByField]
const compareB = b.users[0][sortByField]
if (compareA || compareB) {
if ("DESC" === sortByType) {
let comparison = 0;
if (compareA > compareB) {
comparison = 1;
} else if (compareA < compareB) {
comparison = -1;
}
return comparison;
} else {
let comparison = 0;
if (compareA < compareB) {
comparison = 1;
} else if (compareA > compareB) {
comparison = -1;
}
return comparison;
}
}
})
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: newRows})
}
if (dataAccountsTotal) {
accountContext.accountDispatch({type: SET_ACCOUNTS_TOTAL, payload: dataAccountsTotal})
}
if (dataAccountUsers) {
setParentIDsHandler(dataAccountUsers.accountUsers[0].account.id)
accountContext.accountDispatch({type: GET_PARENT_ACCOUNT, payload: dataAccountUsers})
}
if (dataAccounts) {
console.log("NEW DATA", searchText, startDate, endDate, datesValid)
console.log("SEARCH TEXT", searchText)
console.log("START DATE", startDate)
console.log("END DATE", endDate)
accountContext.accountDispatch({type: SET_ACCOUNTS, payload: dataAccounts})
console.log("DATES VALID", datesValid)
if (startDate
&& endDate && datesValid) {
console.log("FILTER BY DATES")
filterDates(accountContext.accountState.data.accounts)
}
if (searchText) {
filterText(dataAccounts)
}
}
}, [
loadingAccounts,
dataAccounts,
dataAccountsTotal,
dataAccountUsers,
parentAccountIDs,
updateAccountData,
sortByField,
sortByType,
searchText
])
return (
<Fragment>
{
accountContext.accountState.data.accounts &&
!loadingAccountUsers &&
!errorAccountUsers &&
!loadingAccounts &&
!errorAccounts &&
parentAccountIDs &&
accountContext.accountState.data.accountUsers
? <AccountsGrid
setOffset={setOffset}
currentStartDate={startDate}
currentEndDate={endDate}
searchText={searchByChars}
datesValid={datesValid}
resetAccounts={resetAccounts}
setDates={setDates}
clearDates={clearDates}
deactivateUser={deactivateUser}
handleRequestSort={handleRequestSort}
accountRows={accountContext.accountState.data.accounts}
/> : <SpinnerLoader />}
</Fragment>
)
}
export default Accounts
I realize this is an old question, but I had a similar issue and arrived here to an unanswered question. I would like to share what I have learned about useEffect hook with Apollo Query or in any other use case, in the event it helps someone else.
useEffect hook does not actively "watch" and only triggers under select circumstances. Data loading from a graphql query is not one of the triggers. useEffect will run :
After re-rendering of the layout when the component mounts
If the state changes of the component
If the props of the component change
When the component unmounts
Please check out this amazing article for more details.
Also if the variable is placed in the dependency array at the end of the useEffect hook, by design, it will NOT run if there are no changes to the variable.
So in order to trigger useEffect after data is loaded from an Apollo query you would have to make one of those conditions happen when the data loads.
It was much easier in my case to simply use the data after loading because trying to sync setState which is async with data loading which is async created some varied and unpredictable results.
It did help to use the parent class and pass in a variable as a prop which would trigger useEffect to setState when it changed. The variable was used in the actual Apollo query so it made sense to the overall design.
Here is an example:
export const ResultsGrid = ( queryTerm ) => {
const [searchTerm, setSearchTerm] = useState('')
const { data, error, loading } = useQuery(GET_RESULTS_QUERY, {variables: { searchTerm }, })
useEffect(() => {setSearchTerm(queryTerm)}
,[queryTerm]) //useEffect will run only when queryTerm changes
if (loading) return <p>Loading data from query...</p>
if (error)
return (
<React.Fragment>
<p>Sorry, unable to fetch results. </p>
<button onClick={() => refetch}>Please try again!</button>
{console.log("error", error)}
</React.Fragment>
)
console.log('data:', data)
console.log('loading:', loading)
const { results } = data
return (
//display data results here
)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
I hope this is helpful and answers the question.

Resources