简体   繁体   中英

How to re-render React components without actually changing state

In my React application I have a component called Value , which has several instances on multiple levels of the DOM tree. Its value can be shown or hidden, and by clicking on it, it shows up or gets hidden (like flipping a card).

I would like to make 2 buttons, "Show all" and "Hide all", which would make all these instances of the Value component to show up or get hidden. I created these buttons in a component (called Cases ) which is a parent of each of the instances of the Value component. It has a state called mode , and clicking the buttons sets it to "showAll" or "hideAll". I use React Context to provide this chosen mode to the Value component.

My problem: after I click the "Hide All" button and then make some Value instances visible by clicking on them, I'm not able to hide all of them again. I guess it is because the Value components won't re-render, because even though the setMode("hideAll") function is called, it doesn't actually change the value of the state.

Is there a way I can make the Value instances re-render after calling the setMode function, even though no actual change was made? I'm relatively new to React and web-development, I'm not sure if it is the right approach, so I'd also be happy to get some advices about what a better solution would be.

Here are the code for my components:

const ModeContext = React.createContext()

export default function Cases() {
  const [mode, setMode] = useState("hideAll") 
  return (
    <>
        <div>
           <button onClick={() => setMode("showAll")}>Show all answers</button>
           <button onClick={() => setMode("hideAll")}>Hide all answers</button>
        </div>
        <ModeContext.Provider value={mode}>
            <div>
                {cases.map( item => <Case key={item.name} {...item}/> ) }
            </div>
        </ModeContext.Provider>   
    </>
  )
}

export default function Value(props) {
  const mode = useContext(ModeContext)
  const [hidden, setHidden] = useState(mode === "showAll" ? false : true)

  useEffect(() => {
        if (mode === "showAll") setHidden(false)
        else if (mode === "hideAll") setHidden(true)
    }, [mode])

  return (
    hidden 
        ? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span> 
        : <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
  )
}

You first need to create your context before you can use it as a provider or user.

So make sure to add this to the top of the file.

const ModeContext = React.createContext('hideAll')

As it stands, since ModeContext isn't created, mode in your Value component should be undefined and never change.

If your components are on separate files, make sure to also export ModeContext and import it in the other component.

Example

Here's one way to organize everything and keep it simple.

// cases.js

const ModeContext = React.createContext('hideAll')

export default function Cases() {
  const [mode, setMode] = useState("hideAll") 
  return (
    <>
        <div>
           <button onClick={() => setMode("showAll")}>Show all answers</button>
           <button onClick={() => setMode("hideAll")}>Hide all answers</button>
        </div>
        <ModeContext.Provider value={mode}>
            <div>
                {cases.map( item => <Case key={item.name} {...item}/> ) }
            </div>
        </ModeContext.Provider>   
    </>
  )
}

export function useModeContext() {
  return useContext(ModeContext)
}
// value.js

import { useModeContext } from './cases.js'

export default function Value(props) {
  const mode = useContext(ModeContext)
  const [hidden, setHidden] = useState(mode === "showAll" ? false : true)

  useEffect(() => {
        if (mode === "showAll") setHidden(false)
        else if (mode === "hideAll") setHidden(true)
    }, [mode])

  return (
    hidden 
        ? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span> 
        : <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
  )
}

PS I've made this mistake many times, too.

You shouldn't use a new state in the Value component. Your components should have an [only single of truth][1], in your case is mode . In your context, you should provide also a function to hide the components, you can call setHidden

Change the Value component like the following:

export default function Value(props) {
  const { mode, setHidden } = useContext(ModeContext)
  
  if(mode === "showAll") {
        return <span className="hiddenValue" onClick={() => setHidden("hideAll")}></span> 
        } else if(mode === "hideAll") { 
        return <span className="value" onClick={() => setHidden("showAll")}>{props.children}</span>
} else {
 return null; 
}
  )
} 

PS Because mode seems a boolean value, you can switch between true and false. [1]: https://reactjs.org/docs/lifting-state-up.html

There are a few ways to handle this scenario.

  1. Move the state in the parent component. Track all visible states in the parent component like this:

  const [visible, setVisibilty] = useState(cases.map(() => true))
...
<button onClick={() => setVisibilty(casses.map(() => false)}>Hide all answers</button>
...
 {cases.map((item, index) => <Case key={item.name} visible={visible[index]} {...item}/> ) }
  1. Reset the mode after it reset all states:
const [mode, setMode] = useState("hideAll") 
useEffect(() => {
   setMode("")
}, [mode])

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM