React: When the React Portal component uses the useContext, its props.children disappears when the state is updated.

Created on 14 Mar 2019  路  1Comment  路  Source: facebook/react

What is the current behavior?
When the React Portal component uses the useContext, its props.children disappears when the state is updated.If I don't update the state or use Portal, the props.children of Portal will display normally.

If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your JSFiddle (https://jsfiddle.net/Luktwrdm/) or CodeSandbox (https://codesandbox.io/s/new) example below:

const Context = React.createContext({})
const Provider = (props) => {
    const [value, setValue] = useState(0)

    return (
        <Context.Provider
            value={{
                value
            }}
        >
            {props.children}
        </Context.Provider>
    )
}

const Portal = (props) => {
  const el = document.createElement('div')
  const {value} = useContext(Context)
  useEffect(() => {
    document.body.appendChild(el)
    return () => {
      document.body.removeChild(el)
    }
  }, [])

  return ReactDOM.createPortal(
    props.children,
    el
  )
}

const LoadingBar = (props) => {
  return (
    <Provider>
      <Portal>
        <Bar />
      </Portal>
      {props.children}
    </Provider>
  )
}

const Bar = () => {
  const {value} = useContext(Context)

  return (
    <div>{value}</div>
  )
}

const Child = (props) => {
  const { value, setValue } = useContext(Context)
  setValue(5)

  return (<div>child</div>)
}

const App = () => {
  return (
    <LoadingBar>
      <Child></Child>
    </LoadingBar>
  )
}

After updating the "value" in the <Child /> component, the <Bar > component on the page disappears, leaving only the <div /> created by the Portal, deleting "setValue(5)" or the Portal component return<div>{props.children}</div>, the <Bar /> component is displayed again.

What is the expected behavior?

I am expecting Portal's props.children to display properly when using the useContext and the state is updated.

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

React 16.8

Most helpful comment

You're creating a different div on every render, and portal into it:

const Portal = (props) => {
  const el = document.createElement('div')
  // ...
  return ReactDOM.createPortal(props.children, el)
}

However, only initial div gets added to the document:

  useEffect(() => {
    // el here is the div from first render
    document.body.appendChild(el)
    return () => {
      document.body.removeChild(el)
    }
  }, []) // <-- never reruns

So this is why they disappear.

The fix is to use the same portal target for all renders. You can put it in a ref:

const Portal = (props) => {
  const elRef = useRef(null)
  if (!elRef.current) {
    // Create once
    elRef.current = document.createElement('div')
  }
  const el = elRef.current

  const {value} = useContext(Context)
  useEffect(() => {
    document.body.appendChild(el)
    return () => {
      document.body.removeChild(el)
    }
  }, [])

  return ReactDOM.createPortal(
    props.children,
    el
  )
}

Then it persists between renders.

>All comments

You're creating a different div on every render, and portal into it:

const Portal = (props) => {
  const el = document.createElement('div')
  // ...
  return ReactDOM.createPortal(props.children, el)
}

However, only initial div gets added to the document:

  useEffect(() => {
    // el here is the div from first render
    document.body.appendChild(el)
    return () => {
      document.body.removeChild(el)
    }
  }, []) // <-- never reruns

So this is why they disappear.

The fix is to use the same portal target for all renders. You can put it in a ref:

const Portal = (props) => {
  const elRef = useRef(null)
  if (!elRef.current) {
    // Create once
    elRef.current = document.createElement('div')
  }
  const el = elRef.current

  const {value} = useContext(Context)
  useEffect(() => {
    document.body.appendChild(el)
    return () => {
      document.body.removeChild(el)
    }
  }, [])

  return ReactDOM.createPortal(
    props.children,
    el
  )
}

Then it persists between renders.

Was this page helpful?
0 / 5 - 0 ratings