facebookexperimental / Recoil

Recoil is an experimental state management library for React apps. It provides several capabilities that are difficult to achieve with React alone, while being compatible with the newest features of React.
https://recoiljs.org/
MIT License
19.59k stars 1.18k forks source link

Warning: Cannot update a component (`xxx`) while rendering a different component (`xxx`). #12

Open syunto07ka opened 4 years ago

syunto07ka commented 4 years ago

A warning is occured where useRecoilValue() is used even though I write a code like one which is in the official document here.

スクリーンショット 2020-05-15 4 55 26

this is my code

import React, { useEffect } from 'react';
import { atom, useRecoilState, useRecoilValue, selector } from 'recoil';

function RecoilApp() {
  useEffect(() => {
      console.log('test');
  });
  return(
    <div>
      Recoil App
      <TextInput />
      <CharCount />
    </div>
  );
}

const textState = atom({key: 'textState', default: ''});

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = event => {
    setText(event.target.value);
  }

  return(
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
};

function CharCount() {
  const count = useRecoilValue(countState);
  return <>Charset Count: {count}</>;
}

const countState = selector({
  key: 'countState',
  get: ({get}) => {
    const text = get(textState);
    return text.length;
  },
});

export default RecoilApp;
emrekara37 commented 4 years ago

Any update for this issue

8detect commented 4 years ago

Any update for this issue

unfortunately not yet ! so live with the warning without any hacking around ( should not ) ! the error must be fixed in the core of the lib not working around , so until then just ignore the warning, at least it works and nothing crashes (hopefully), i still use it in my beta project. I forgot to mention that i use snowpack and so annoyed that the package has not supported ES module, hope the author fix it soon in late of this month because Recoil is really cool !

mmmoli commented 4 years ago

Fix is in the v0.0.11 release so keep watch for that.

Proposed: 15-Sep-2020 (https://github.com/facebookexperimental/Recoil/issues/534#issuecomment-691580341)

adrianbw commented 4 years ago

I'm seeing this in 0.0.11 still. Here's a sample set of tests that are throwing it:

describe("Container", () => {
  it("renders expected markup", () => {
    const { container } = mount();
    expect(container).toMatchSnapshot();
  });
  it("does not have accessibility violations", async () => {
    const { container } = mount();
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});
philohelp commented 4 years ago

Fixed in v0.0.13 Youhouhou

yukitaka13-1110 commented 4 years ago

v0.0.13 works for me! Excellent!!

8detect commented 4 years ago

confirmed : all fixed : the warning; useRecoilState bug with writable atom ( selector ) ; ESM support, web_modules compatible for snowpack. Thank you !

adrianbw commented 4 years ago

Can I see example of a test that's working for someone? I'm now on 0.0.13 and seeing the Batcher warning on this:

describe("IrrigationEditTable", () => {
  it("renders correctly", () => {
    let wrapper;
    act(() => {
      wrapper = renderedComponent();
    });
    expect(wrapper).toMatchSnapshot();
  });
});
akshayDatacode commented 4 years ago

I am Also Facing the same warning in my console

hatpick commented 4 years ago

@drarmstr @davidmccabe I can see that 0.13 is published on npm, but it's not listed under releases page here, can we see a change log?

sidferreira commented 4 years ago

this helped me to solve my issue. I had a callback inside a memoed component but it wasn't using useEffect

drarmstr commented 4 years ago

@drarmstr @davidmccabe I can see that 0.13 is published on npm, but it's not listed under releases page here, can we see a change log?

Yes, #581 is still landing

Tobijudah commented 4 years ago

I found this error when:

  • I had a setState in parent component (e.g const [state, setState] = useState({}))
  • I passed this setState to a child component and used it in a onClick={setState("value")}
  • I solved this problem adding the arrow function in the onClick call

this worked for me. @DaniCastel any reason an arrow function had to be used? React newbie here

pranavkumar389 commented 3 years ago

In my case, it's because I was mutating a prop value in the child component.

BEFORE

import React from 'react';

const Todo = ({ text, todo, todos, setTodos }) => {

  const deleteHandler = () => {
    setTodos(todos.filter(el => el.id !== todo.id)) // mutauing a prop here
  }

 return (
   <div className="todo">
     <li className="todo-item">{text}</li>
     <button className="complete-btn"><i className="fas fa-check"></i></button>
     <button className="trash-btn" onClick={deleteHandler()}><i className="fas fa-trash"></i></button>
   </div>
 );
}

export default Todo;

AFTER

import React from 'react';

const Todo = ({ text, todo, todos, setTodos }) => {

  const todosCopy = JSON.parse(JSON.stringify(todos));

  const deleteHandler = () => {
    setTodos(todosCopy.filter(el => el.id !== todo.id)) // mutating a copy of prop value here
  }

 return (
   <div className="todo">
     <li className="todo-item">{text}</li>
     <button className="complete-btn"><i className="fas fa-check"></i></button>
     <button className="trash-btn" onClick={deleteHandler}><i className="fas fa-trash"></i></button>
   </div>
 );
}

export default Todo;
FezVrasta commented 3 years ago

@pranavkumar389 Array.prototype.filter doesn't mutate its original array.

delucca commented 3 years ago

Any updates at this issue?

I'm facing it in my custom IntlProvider. In a nutshell, I'm wrapping it (from react-intl) in a custom component that stores the selected locale in my Recoil state for further usage by my own selectors.

Here is a sample code:

import React, { ReactElement } from 'react'
import { IntlProvider } from 'react-intl'
import { OptionalIntlConfig } from 'react-intl/src/components/provider'
import { useSetRecoilState } from 'recoil'

import { locale as localeAtom } from 'state/recoil/intl/locale'

export interface RecoilIntlProviderProps extends OptionalIntlConfig {
  children: ReactElement
}

const RecoilIntlProvider = (props: RecoilIntlProviderProps): ReactElement => {
  const setRecoilIntl = useSetRecoilState(localeAtom)
  setRecoilIntl(props.locale)

  return <IntlProvider {...props} />
}

export default RecoilIntlProvider

And my atom:

import { atom } from 'recoil'

import { PREFIX } from './constants'

const KEY = `${PREFIX}::LOCALE`

export const locale = atom<string>({
  key: KEY,
  default: '',
})

To make it work, you need to use the following tree in your app.js(ts):

<RecoilRoot>
  <RecoilIntlProvider locale={locale ?? 'pt-BR'} messages={messages}>
    <Component {...pageProps} />
  </RecoilIntlProvider>
</RecoilRoot>

And my warning in the console log:

Warning: Cannot update a component (`Batcher`) while rendering a different component (`RecoilIntlProvider`). To locate the bad setState() call inside `RecoilIntlProvider`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
RecoilIntlProvider@webpack-internal:///./components/Base/RecoilIntlProvider/recoil-intl-provider.tsx:23:87
RecoilRoot@webpack-internal:///../.yarn/$$virtual/recoil-virtual-0c2fe5134c/0/cache/recoil-npm-0.1.2-9a0edbd2b9-c69105dd7d.zip/node_modules/recoil/es/recoil.js:1722:1
BudApp@webpack-internal:///./pages/_app.tsx:70:19
ErrorBoundary@webpack-internal:///../.yarn/$$virtual/@next-react-dev-overlay-virtual-ba69454c0b/0/cache/@next-react-dev-overlay-npm-10.0.0-5db65d7be6-f38cbe7f69.zip/node_modules/@next/react-dev-overlay/lib/internal/ErrorBoundary.js:23:47
ReactDevOverlay@webpack-internal:///../.yarn/$$virtual/@next-react-dev-overlay-virtual-ba69454c0b/0/cache/@next-react-dev-overlay-npm-10.0.0-5db65d7be6-f38cbe7f69.zip/node_modules/@next/react-dev-overlay/lib/internal/ReactDevOverlay.js:73:20
Container@webpack-internal:///../.yarn/$$virtual/next-virtual-6f4174843d/0/cache/next-npm-10.0.0-82dc2f1372-c01b177cb2.zip/node_modules/next/dist/client/index.js:173:20
AppContainer@webpack-internal:///../.yarn/$$virtual/next-virtual-6f4174843d/0/cache/next-npm-10.0.0-82dc2f1372-c01b177cb2.zip/node_modules/next/dist/client/index.js:641:18
Root@webpack-internal:///../.yarn/$$virtual/next-virtual-6f4174843d/0/cache/next-npm-10.0.0-82dc2f1372-c01b177cb2.zip/node_modules/next/dist/client/index.js:757:18

Looking forward for a fix in this issue :)

nickhow83 commented 3 years ago

I had a similar issue and it turns out the problem was how I was using it. I had an async selector which derived a value from an atom.

For example, I had a User atom and a Roles async selector. Whenever the User updated (should really only happen once) then the Roles would be loaded from the API for that User.

However, I used a hook to retrieve the User and every time the hook ran, it updated the User (in the hook), which in turn triggered a Roles API request.

Simple fix was to set the User atom from a useEffect, with the User in the deps array.

chroth7 commented 3 years ago

I had a similar issue just now and solved it the very same way @nickhow83 did. So in my case:

nickhow83 commented 3 years ago

Any updates at this issue?

I'm facing it in my custom IntlProvider. In a nutshell, I'm wrapping it (from react-intl) in a custom component that stores the selected locale in my Recoil state for further usage by my own selectors.

Here is a sample code:

import React, { ReactElement } from 'react'
import { IntlProvider } from 'react-intl'
import { OptionalIntlConfig } from 'react-intl/src/components/provider'
import { useSetRecoilState } from 'recoil'

import { locale as localeAtom } from 'state/recoil/intl/locale'

export interface RecoilIntlProviderProps extends OptionalIntlConfig {
  children: ReactElement
}

const RecoilIntlProvider = (props: RecoilIntlProviderProps): ReactElement => {
  const setRecoilIntl = useSetRecoilState(localeAtom)
  setRecoilIntl(props.locale)

  return <IntlProvider {...props} />
}

export default RecoilIntlProvider

And my atom:

import { atom } from 'recoil'

import { PREFIX } from './constants'

const KEY = `${PREFIX}::LOCALE`

export const locale = atom<string>({
  key: KEY,
  default: '',
})

To make it work, you need to use the following tree in your app.js(ts):

<RecoilRoot>
  <RecoilIntlProvider locale={locale ?? 'pt-BR'} messages={messages}>
    <Component {...pageProps} />
  </RecoilIntlProvider>
</RecoilRoot>

And my warning in the console log:

Warning: Cannot update a component (`Batcher`) while rendering a different component (`RecoilIntlProvider`). To locate the bad setState() call inside `RecoilIntlProvider`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
RecoilIntlProvider@webpack-internal:///./components/Base/RecoilIntlProvider/recoil-intl-provider.tsx:23:87
RecoilRoot@webpack-internal:///../.yarn/$$virtual/recoil-virtual-0c2fe5134c/0/cache/recoil-npm-0.1.2-9a0edbd2b9-c69105dd7d.zip/node_modules/recoil/es/recoil.js:1722:1
BudApp@webpack-internal:///./pages/_app.tsx:70:19
ErrorBoundary@webpack-internal:///../.yarn/$$virtual/@next-react-dev-overlay-virtual-ba69454c0b/0/cache/@next-react-dev-overlay-npm-10.0.0-5db65d7be6-f38cbe7f69.zip/node_modules/@next/react-dev-overlay/lib/internal/ErrorBoundary.js:23:47
ReactDevOverlay@webpack-internal:///../.yarn/$$virtual/@next-react-dev-overlay-virtual-ba69454c0b/0/cache/@next-react-dev-overlay-npm-10.0.0-5db65d7be6-f38cbe7f69.zip/node_modules/@next/react-dev-overlay/lib/internal/ReactDevOverlay.js:73:20
Container@webpack-internal:///../.yarn/$$virtual/next-virtual-6f4174843d/0/cache/next-npm-10.0.0-82dc2f1372-c01b177cb2.zip/node_modules/next/dist/client/index.js:173:20
AppContainer@webpack-internal:///../.yarn/$$virtual/next-virtual-6f4174843d/0/cache/next-npm-10.0.0-82dc2f1372-c01b177cb2.zip/node_modules/next/dist/client/index.js:641:18
Root@webpack-internal:///../.yarn/$$virtual/next-virtual-6f4174843d/0/cache/next-npm-10.0.0-82dc2f1372-c01b177cb2.zip/node_modules/next/dist/client/index.js:757:18

Looking forward for a fix in this issue :)

Your provider is setting the state in the render. If you put it in a useEffect then the problem will go away :)

It seems it’s not a problem with the library but the understanding of how it’s used in the react lifecycle.

If you had a useState for example, you wouldn’t update that directly in the render method, you’d do it inside an effect.

onur-kaplan commented 3 years ago

@syunto07ka I have the same problem, but when I run the "npm run prod" code, this error disappears. I only see this error in development mode.

nickhow83 commented 3 years ago

@syunto07ka I have the same problem, but when I run the "npm run prod" code, this error disappears. I only see this error in development mode.

Running in prod mode only hides the errors, it still happens. Have a look at my comment above and it should explain what needs to be done

JituG22 commented 3 years ago

In my case the problem was with "redux" reducer file declaration where i was did some bad practice..! and after long time debug I got fix it.

below was code Issue/bad statement which i was passed ..! myReducer.js

switch(action.type){

case "Any_Case": return { data:action.payload, } }

Understand this issue : If you see in above code snippet i was miss out to add in return "...state" which was the coz issue produce. If you understand issue that if you not add "...state" in return it will whole state replace with new state and all previous state get overwrite/replace/vanish with new one state which Coz to producer issue that "Cannot update a component (xxx) while rendering a different component (xxx)."

So here is the solution add ...state statement in code

myReducer.js

switch(action.type){

case "Any_Case": return { ...state, / make sure add this line of code to fix issue/ data:action.payload, } }

delucca commented 3 years ago

I'm facing this error for some months for now, no idea how to fix it?

Until now it was not causing any trouble in my application at all. And since prod "hides" those errors, it was not a big deal. But, now I'm coding some unit tests for bigger modules (using Jest), and this kind of error is causing strange behavior such as one component inside the component tree is correctly updated with the new data (after a setState from a parent component) but other component isn't.

To give more details, the component I'm trying to code has basically three parts:

  1. The parent, which does a GraphQL query and updates my Recoil state after the query finishes
  2. A child, that renders part of that query data
  3. Another child (at the same level as the latest child) that contains a simple component (grandchild of the parent) which renders another part of the query data

While running the app, it works like a charm. It throws that Batcher error, but it works.

But, while testing it with Jest + Enzyme, it simply won't work. The first child is updated with the new state, but the second one isn't.

In the parent component I update the recoil state with the onCompleted callback option of Apollo (but the same issue happens with useEffect)

Here are a minimal reproduction of those 3 components:

parent:

const SettingsAccount = () => {
  const myUserID = useRecoilValue(meAtom)
  const setUser = useSetRecoilState(userAtomFamily(myUserID))
  const [getUserData, { loading, variables }] = useLazyQuery<GetUserDataQuery>(
    queries.GET_USER_DATA,
    {
      onCompleted: (data) => setUser(data.user),
      variables: {
        id: myUserID,
      },
    },
  )

  useEffect(() => {
    if (myUserID && myUserID !== variables?.id) getUserData()
  }, [myUserID, getUserData, variables])

  return (
    <Flex py={4} gridGap={6} direction="column" w="full">
      <SettingsAccountHeader userID={myUserID} loading={loading} />
      <Divider borderColor="black.200" />
      <SettingsAccountBody userID={myUserID} loading={loading} />
    </Flex>
  )
}

child 1 (SettingsAccountHeader)

const SettingsAccountHeader = ({ userID, loading }: SettingsAccountHeader) => {
  const user = useRecoilValue(userAtomFamily(userID))
  const isLoaded = !loading && Boolean(user)

  return (
    <Flex gridGap={4} alignItems="center">
      <UserAvatar size="xl" name={user?.fullName} src={user?.picture} />

      <Flex direction="column" gridGap={4}>
        <Flex direction="column" gridGap={1}>
          <Skeleton isLoaded={isLoaded} {...buildSkeletonMinSize(isLoaded, 180, 24)}>
            <Heading as="h2" color="black.900" fontSize="xl" fontWeight={500}>
              {user?.fullName}
            </Heading>
          </Skeleton>

          <Skeleton isLoaded={isLoaded} {...buildSkeletonMinSize(isLoaded, 220, 18)}>
            <Text color="gray.400" fontSize="md" fontWeight={400}>
              {user?.role}
            </Text>
          </Skeleton>
        </Flex>

        <UserTeamTags userID={userID} loading={loading} />
      </Flex>
    </Flex>
  )
}

child 2 (SettingsAccountBody)

const SettingsAccountBody = ({ userID, loading }: SettingsAccountBodyProperties) => (
  <Flex direction="column" gridGap={6}>
    <SettingsAccountBodyPersonalInformations userID={userID} loading={loading} />
    <Divider borderColor="black.200" />
    <SettingsAccountBodySocialMedia userID={userID} loading={loading} />
  </Flex>
)

first grandchild (SettingsAccountBodyPersonalInformations)

const SettingsAccountBodyPersonalInformations = ({
  userID,
  loading,
}: SettingsAccountBodyPersonalInformationsProperties) => {
  const user = useRecoilValue(userAtomFamily(userID))
  const intl = useIntl()

  const isLoaded = !loading && Boolean(user)

  return (
    <Stack direction="column" spacing={6}>
      <SettingsAccountBodySectionTitle
        title={intl.formatMessage(messages.sectionTitle)}
        subtitle={intl.formatMessage(messages.sectionSubtitle)}
      />

      <Stack direction="column" spacing={4}>
        <Flex>
          <EditableField
            label={intl.formatMessage(messages.firstFieldLabel)}
            value={user?.firstName}
            isLoaded={isLoaded}
            flexGrow={1}
          />
          <EditableField
            label={intl.formatMessage(messages.secondFieldLabel)}
            value={user?.lastName}
            isLoaded={isLoaded}
            flexGrow={1}
          />
        </Flex>

        <EditableField
          label={intl.formatMessage(messages.thirdFieldLabel)}
          value={user?.nickname}
          isLoaded={isLoaded}
        />

        <EditableField label={intl.formatMessage(messages.fourthFieldLabel)}>
          <UserTeamTags userID={userID} loading={loading} />
        </EditableField>

        <EditableField
          label={intl.formatMessage(messages.fifthFieldLabel)}
          value={user?.role}
          isLoaded={isLoaded}
        />

        <EditableField
          label={intl.formatMessage(messages.sixthFieldLabel)}
          value={user?.gender}
          isLoaded={isLoaded}
        />

        <EditableField
          label={intl.formatMessage(messages.seventhFieldLabel)}
          value={user?.about}
          isLoaded={isLoaded}
        />
      </Stack>
    </Stack>
  )
}

It is a simple implementation, with just a single component changing the state (the root parent), but it still wont work. If I do console.log(user) inside of SettingsAccountHeader (child 1) I can get the correct data. But, if I do the same console.log(user) inside SettingsAccountBodyPersonalInformations (the grandchild) it prints undefined.

It does not make sense. Since both components are only reading the state (without changing it at all) why they can't print the correct results in my test environment?

Just for the record, SettingsAccountBodySocialMedia component (the component I've not shared here, is almost the same as grandchild 1), it does not mutate state or anything.

AndrewJudson commented 3 years ago

If I get this warning once and everything else is working as expected, should I be concerned? Does it indicate a memory leak or something else that will affect the performance of the app?

delucca commented 3 years ago

If I get this warning once and everything else is working as expected, should I be concerned? Does it indicate a memory leak or something else that will affect the performance of the app?

I was in your shoes last year. I was getting this warning, and everything else was ok. My problem was when I started coding unit tests. Them, this warning was preventing my recoil state from being updated on test environments.

So, this is still problem.

ryancwalsh commented 3 years ago

I'm having this problem too!

thexpand commented 3 years ago

For me it was a stupid mistake (typo). I had a memoized function, which inside of its dependencies had a Recoil setter being invoked/called, instead of just passed in the deps array.

Problematic code:

const [, setHasUnsavedChanges] = useRecoilState(hasUnsavedChangesState);

const save = useCallback(
    () => {
        // ...
    },
    [setHasUnsavedChanges()] // <--- Here's what was wrong, function was invoked, which is wrong
);

Fixed code:

const [, setHasUnsavedChanges] = useRecoilState(hasUnsavedChangesState);

const save = useCallback(
    () => {
        // ...
    },
    [setHasUnsavedChanges] // <--- Here it is fixed
);
alex-cory commented 3 years ago

I'm working on a big refactor where dependencies will not go through React state at all. That will fix this (and make it a lot more efficient, and is needed for CM). In the meantime I don't see a an obvious workaround unfortunately.

@davidmccabe Any idea on a rough timeline for when we can expect this refactor to be working?

a-tonchev commented 3 years ago

Same here on React 17.0.2 and recoil 0.3.1.

Is it safe to ignore this warning?

gilvaju commented 3 years ago

Same here on React 17.0.2 and recoil 0.3.1.

Is it safe to ignore this warning?

+1

obadakhalili commented 3 years ago

Same in React 17.0.2 and Recoil 0.3.1.

In my case, I'm using a Recoil setter inside the useMemo hook. That should be fine, though.

codal-hkrishnani commented 3 years ago

I was facing same issue, The fix worked for me was if u are doing

setParams/setOptions

outside of useEffect then this issue is occurring. So try to do such things inside useEffect. It'll work like charm

a-tonchev commented 3 years ago

I was facing same issue, The fix worked for me was if u are doing

setParams/setOptions

outside of useEffect then this issue is occurring. So try to do such things inside useEffect. It'll work like charm

For me this does not fix the problem. Actually sometimes the warning appears, sometimes not. it is weird ...

codal-hkrishnani commented 3 years ago

I was facing same issue, The fix worked for me was if u are doing

setParams/setOptions

outside of useEffect then this issue is occurring. So try to do such things inside useEffect. It'll work like charm

For me this does not fix the problem. Actually sometimes the warning appears, sometimes not. it is weird ...

Try to check in all screens, you might have add same code in multiple screen screens

davidh99720 commented 3 years ago

I have the same issue. It is caused by setting pageTitle (displayed in explorer tab and history) in each component after routing using recoil. Fixed after centralizing the pageTitle logic in the root of component where pageTitle is rendered and get rid of the recoil setting action.

feel5ny commented 1 year ago

Same here on React 17.0.2 and recoil 0.3.1.

Is it safe to ignore this warning? @davidmccabe

+2

image

incepter commented 1 year ago

No, it is not safe to ignore, it is against the model of react itself.

Your component while rendering tries to alter the state of another component, this couples both of them to each other and makes the render operation of your component non atomic and with side effects impacting other components.

Recoil uses some technique internally (for batching if my memory is good enough) that alters the state of components while rendering, and react tells us not to do this, so clearly you should not ignore the message.

AFAIK, it was related to selectors, but I remember only the initial ever code about recoil, I don't honestly know how this became and if there is a chance to solve this issue.

Zasa-san commented 1 year ago

+1 Having the same issue right now using React 18.0.25 & Recoil 0.7.6.