zsofi
zsofi

Reputation: 1

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>
  )
}

Upvotes: 0

Views: 776

Answers (3)

Domino987
Domino987

Reputation: 8774

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])

Upvotes: 0

Nick
Nick

Reputation: 6362

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>
  )
}

P.S. I've made this mistake many times, too.

Upvotes: 1

Giovanni Mazzuoccolo
Giovanni Mazzuoccolo

Reputation: 91

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; 
}
  )
} 

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

Upvotes: 0

Related Questions