using Fomik hook `useField` and `react-data-table-component` causes infinite loop - formik

I am using react-data-table-component inside formik form to update new values. But the problem is whenever the MyTable component is re-rendered, the selectableRowSelected() callback is called, which triggers onSelectedRowsChange event in which I use helpers.setValue() to set value, which then makes MyTable component renders again. This whole process causes infinite loop, and I still don't have a solution for this.
function MyTable() {
const [data, setData] = useState([]);
const [field, meta, helpers] = useField({ name: "use" });
useEffect(() => {
fetch("https://reqres.in/api/users?page=1&per_page=3")
.then((res) => res.json())
.then((res) => setData(res.data));
}, []);
const handleChange = React.useCallback(({ selectedRows }) => {
let selectedIds = selectedRows.map(function (row) {
return parseInt(row.id);
});
selectedIds.sort();
console.log("🚀 ~ selectedIds", selectedIds);
// helpers.setValue(selectedIds, true); --> uncomment this will cause infinite loop.
}, []);
return (
<DataTable
title="User List"
columns={columns}
data={data}
selectableRows
selectableRowsHighlight
onSelectedRowsChange={handleChange}
selectableRowSelected={(row) => {
return meta.value.includes(parseInt(row.id));
}}
/>
);
}
CodeSandbox: https://codesandbox.io/s/goofy-tu-l1pxvb?file=/src/MyTable.jsx:375-1249

I think I've figured it out myself. But I will post it here in case anyone may encounter the same.
There is no way to stop this problem as the way RDT works is whenever selectableRowSelected is called, it will dispatch an action with type: SELECT_MULTIPLE_ROWS:
dispatch({
type: 'SELECT_MULTIPLE_ROWS',...
});
then in tableReducer, it returns toggleOnSelectedRowsChange as boolean value:
case 'SELECT_MULTIPLE_ROWS': {
...
return {
...state,
...,
toggleOnSelectedRowsChange,
};
which controls the trigger of onSelectedRowsChange event (as mentioned at comment in the source code):
useDidUpdateEffect(() => {
onSelectedRowsChange({ allSelected, selectedCount, selectedRows: selectedRows.slice(0) });
// onSelectedRowsChange trigger is controlled by toggleOnSelectedRowsChange state
}, [toggleOnSelectedRowsChange]);
Overall, solution for this problem is don't use formik with RDT for row selection, use another datatable lib.

Related

Dispatch actions from a custom hook using useQuery

I'm trying to write a custom hook that uses useQuery from react-query. The custom hook takes in the id of an employee and fetches some data and returns it to the consuming component. I want to be able to dispatch a redux action to show a loading indicator or show an error message if it fails. Here is my custom hook.
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
}
const query = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
})
if (query.isFetching || query.isLoading) {
dispatch(setWaiting())
}
return query.data
}
When I refresh the page, I get this error in the browser's console and I'm not sure how to fix this error?
Warning: Cannot update a component (`WaitIndicator`) while rendering a different component (`About`).
To locate the bad setState() call inside `About`, follow the stack trace as described in
The issue is likely with dispatching the setWaiting action outside any component lifecycle, i.e. useEffect. Move the dispatch logic into a useEffect hook with appropriate dependency.
Example:
export default function useEmployee(id) {
const initial = {
name: '',
address: '',
};
const { data, isFetching, isLoading } = useQuery(['fetchEmployee', id], () => getEmployee(id), {
initialData: initial,
onSettled: () => dispatch(clearWaiting()),
onError: (err) => dispatch(showError(err)),
});
useEffect(() => {
if (isFetching || isLoading) {
dispatch(setWaiting());
}
}, [isFetching, isLoading]);
return data;
}

Can't get the first value by using useState in a functionn

I need to show the text according to the data value. By running the code, I want to see the 'Test1: 1' can be shown after I clicked the button, but I can't. Any method to make this happen?
Below is a sample sandbox link including the code.
https://codesandbox.io/s/restless-wildflower-9pl09k?file=/src/Parent.js
export default function Parent(props) {
const [data, setData] = useState(0);
const onClick = () => {
setData(1);
console.log(data);
setData(2);
};
return (
<>
<button onClick={onClick}> Click here </button>
{data === 1 ? <div>Test1: {data}</div> : <div>Test2: {data}</div>}
</>
);
}
The setState function returned by useState does not directly update the state. Instead it is used to send the value that React will use during the next asynchronous state update. console.log is an effect so if you want to see data logged every time it is changed, you can use React.useEffect. Run the code below and click the 0 button several times to see the state changes and effects in your browser.
function App() {
const [data, setData] = React.useState(0)
React.useEffect(_ => console.log("data", data), [data])
return <button
onClick={_ => setData(data + 1)}
children={data}
/>
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Your comment talks about a network request example. Custom hooks can be designed to accommodate complex use cases and keep your components easy to write.
function App() {
const [{fetching, error, data}, retry] = useAsync(_ => {
return fetch("https://random-data-api.com/api/users/random_user")
.then(res => res.json())
}, [])
if (fetching) return <pre>Loading...</pre>
if (error) return <pre>Error: {error.message}</pre>
return <div>
<button onClick={retry} children="retry" />
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
}
function useAsync(f, deps) {
const [state, setState] = React.useState({fetching: true})
const [ts, setTs] = React.useState(Date.now())
React.useEffect(_ => {
f()
.then(data => setState({fetching: false, data}))
.catch(error => setState({fetching: false, error}))
}, [...deps, ts])
return [
state,
_ => {
setState({fetching: true, error: null, data: null})
setTs(Date.now())
}
]
}
ReactDOM.render(<App/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
The reason the console.log(data) did not reflect the latest data is because of the React manages state. Calls to setState() are asynchronous, and if you want to rely on the new value of the state, the correct way is to pass a function of old state, returning the current state. ref. documentation

graphql after running mutation if I quickly goback to Previous page, occur error

after running mutation using the graphql, if I quickly goback to Previous page,
occur error : Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and
asynchronous tasks in %s.%s, a useEffect cleanup function,
I think it's because I quickly go to another page during the mutation.
If this is not the case, there is no error.
(Even if an error occurs, update succeeds. but I'm worried about errors)
Even if move to another page during mutating, I want to proceed with the update as it is
How can I proceed with the update?
if If there is no way, is there method that How to create a delay during mutating
im so sorry. my english is not good.
const CalendarTodo = ({
month,
day,
data,`enter code here`
isImportWhether,
setIsImportWhether
}) => {
const [value, setValue] = useState("");
const monthDay = `${month + 1}월 ${day}일`;
const [createToDoMutation] = useMutation(CREATE_TODO, {
variables: {
toDoId:
data &&
data.toDos &&
data.toDos.filter(object => object.monthDay === monthDay)[0] &&
data.toDos.filter(object => object.monthDay === monthDay)[0].id,
monthDay: monthDay,
dayToDo: value,
importEvent: isImportWhether
},
update: (proxy, { data: { createToDo } }) => {
const data = proxy.readQuery({ query: SEE_TODO_OF_ME });
data &&
data.toDos &&
data.toDos.filter(object => object.monthDay === monthDay)[0] &&
data.toDos
.filter(object => object.monthDay === monthDay)[0]
.dayToDo.push(createToDo);
proxy.writeQuery({ query: SEE_TODO_OF_ME, data });
},
optimisticResponse: {
createToDo: {
__typename: "DayToDo",
id: Math.random().toString(),
toDoList: value,
importEvent: isImportWhether
}
}
});
return (
<>
);
};
export default CalendarTodo;
As you already guessed the reason is the asynchronous request that keeps on running even after un-mounting the component due to navigating away from it.
There are many ways to solve this. One is to add a check whether or not the component you are calling the async request from is still mounted and only update its state if so, e.g.:
useEffect(() => {
let isMounted = true;
apollo.mutate({query, variables, update: {
if(isMounted) {
// update state or something
}
})
return () => {
isMounted = false;
};
}, []);
This way however the data might be lost. If you want to make sure that you receive and store the return value you should add the request to a higher level component or context hat will not be unmounted on navigation. This way you can trigger the async call but dont have to worry about navigating away.

How to use most recent state from useReducer before re-render

I have two reducer actions that I want to dispatch one after the other. The first one modifies the state, then the second one uses a portion of the modified state to make another modification. The difficulty is that when the second dispatch is called, it still has the old outdated state and thus doesn't update the state properly.
An example is the following (also found here - https://codesandbox.io/s/react-usereducer-hqtc2) where there is a list of conversations along with a note of which one is considered the "active" conversation:
import React, { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "removeConversation":
return {
...state,
conversations: state.conversations.filter(
c => c.title !== action.payload
)
};
case "setActive":
return {
...state,
activeConversation: action.payload
};
default:
return state;
}
};
export default function Conversations() {
const [{ conversations, activeConversation }, dispatch] = useReducer(
reducer,
{
conversations: [
{ title: "James" },
{ title: "John" },
{ title: "Mindy" }
],
activeConversation: { title: "James" }
}
);
function removeConversation() {
dispatch({ type: "removeConversation", payload: activeConversation.title });
dispatch({ type: "setActive", payload: conversations[0] });
}
return (
<div>
Active conversation: {activeConversation.title}
<button onClick={removeConversation}>Remove</button>
<ul>
{conversations.map(conversation => (
<li key={conversation.title}>{conversation.title}</li>
))}
</ul>
</div>
);
}
In here, when I click the "remove conversation" button, I want to remove the active conversation, then set the active conversation to be the one at the top of the list. However, here when the first dispatch removes the conversation from the list, the second dispatch sets active to conversations[0], which still contains the removed value (since the state hasn't updated yet). As a result, it keeps the active conversation as the one it was before, even though it's been removed from the list.
I could probably combine the logic into just one action and do it all there (remove the conversation and set active all in one), but I would ideally like to keep my reducer actions to have one responsibility each if possible.
Is there any way to make the second dispatch call have the most recent version of the state so that this kind of problem doesn't occur?
It may help if you think of useEffect() like setState's second parameter (from class based components).
If you want to do an operation with the most recent state, use useEffect() which will be hit when the state changes:
const {
useState,
useEffect
} = React;
function App() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count-1);
const increment = () => setCount(count+1);
useEffect(() => {
console.log("useEffect", count);
}, [count]);
console.log("render", count);
return (
<div className="App">
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Some further info on useEffect()
Answering this for anyone who may come across similar issues in the future. The key to finding the solution to this is understanding that state in React is a snapshot.
You can see that in the dispatched setActive action, the value of conversations[0] of state is being passed:
dispatch({ type: "setActive", payload: conversations[0] });
Thus when the action is called before the next render, it uses the snapshotted state at the time of re-render:
// snapshot of state when action is called
{
conversations: [
{ title: "James" },
{ title: "John" },
{ title: "Mindy" }
],
activeConversation: { title: "James" }
}
Thus conversations[0] evaluates to {title: "James"}. This is why in the reducer, activeConversation: action.payload returns {title: "James"} and the active conversation doesn't change. In technical terms, "you're calculating the new state from the value in your closure, instead of calculating it from the most recent value."
So how do we fix this? Well useReducer actually in fact always has access to the most recent state value. It is a sister pattern to the state updater function, which also gives you access to the latest state variable even before the next render.
This means that after the first dispatch action:
dispatch({ type: "removeConversation", payload: activeConversation.title }); // first dispatch action
dispatch({ type: "setActive", payload: conversations[0] }); // second dispatch action
the next dispatch action actually has access to the latest state already. You just need to access it:
case "setActive":
return {
...state,
activeConversation: state.conversations[0]
};
You can verify this by logging it to the console:
const reducer = (state, action) => {
console.log(state);
switch (action.type) {
case "removeConversation":
return {
...state,
conversations: state.conversations.filter(
c => c.title !== action.payload
)
};
case "setActive":
return {
...state,
activeConversation: state.conversations[0]
};
default:
return state;
}
};
Also important to note that the 2 dispatch calls are batched as explained in the state updater function link mentioned above. More info on batching here too.

How do you dynamically control react apollo-client query initiation?

A react component wrapped with an apollo-client query will automatically initiate a call to the server for data.
I would like to fire off a request for data only on a specific user input.
You can pass the skip option in the query options - but this means the refetch() function is not provided as a prop to the component; and it appears that the value of skip is not assessed dynamically on prop update.
My use is case is a map component. I only want data for markers to be loaded when the user presses a button, but not on initial component mount or location change.
A code sample below:
// GraphQL wrapping
Explore = graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
skip: ({ targetResource, searchIsAllowedForMapArea }) => {
const skip = Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE');
return skip;
},
}),
props: ({ ownProps, data: { loading, viewer, refetch }}) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})(Explore);
To include an HoC based on a condition affected by a props change, you could use branch from recompose.
branch(
test: (props: Object) => boolean,
left: HigherOrderComponent,
right: ?HigherOrderComponent
): HigherOrderComponent
check: https://github.com/acdlite/recompose/blob/master/docs/API.md#branch
For this specific example, would look something like:
const enhance = compose(
branch(
// evaluate condition
({ targetResource, searchIsAllowedForMapArea }) =>
Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE'),
// HoC if condition is true
graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
}),
props: ({ ownProps, data: { loading, viewer, refetch } }) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})
)
);
Explore = enhance(Explore);
I have a similar use case, I wanted to load the data only when the user clicked.
I've not tried the withQuery suggestion given by pencilcheck above. But I've seen the same suggestion elsewhere. I will try it, but in the meantime this is how I got it working based off a discussion on github:
./loadQuery.js
Note: I'm using the skip directive:
const LOAD = `
query Load($ids:[String], $skip: Boolean = false) {
things(ids: $ids) #skip(if: $skip) {
title
}
`
LoadMoreButtonWithQuery.js
Here I use the withState higher-order function to add in a flag and a flag setter to control skip:
import { graphql, compose } from 'react-apollo';
import { withState } from 'recompose';
import LoadMoreButton from './LoadMoreButton';
import LOAD from './loadQuery';
export default compose(
withState('isSkipRequest', 'setSkipRequest', true),
graphql(
gql(LOAD),
{
name: 'handleLoad',
options: ({ids, isSkipRequest}) => ({
variables: {
ids,
skip: isSkipRequest
},
})
}
),
)(Button);
./LoadMoreButton.js
Here I have to manually "flip" the flag added using withState:
export default props => (
<Button onClick={
() => {
props.setSkipRequest(false); // setter added by withState
props.handleLoad.refetch();
}
}>+</Button>
);
Frankly I'm a little unhappy with this, as it is introduces a new set of wiring (composed in by "withState"). Its also not battle tested - I just got it working and I came to StackOverflow to check for better solutions.

Resources