DEV Community

Zeyad Etman
Zeyad Etman

Posted on

Apply condition on specific nested child elements in ReactJS

Published here: Apply condition on specific nested child elements in ReactJS

Apply condition on specific nested child elements in ReactJS

Intro

We have a reactjs app, this app contains a container component called App inside it, we have multiple nested components some of them contains button elements. What we want to do is disable all child buttons programmatically without using styles way when I toggle a switch button in the container component App.

Naïve Solution

The naïve solution to this problem is simple create a new state to store the value of the switch button, then iterate manually over each child components and searching for button, if you find a button pass the switch button state value to the component to update the button behavior whenever the switch changes <Button {...(isDisable ? { disabled: true } : {})} />.

If we have 10 nested buttons for example, it'll be hassle to iterate over them, also we may add more buttons or change the condition.

take a look of the Tab 1 Panel 1 button here: https://github.com/zeyadetman/action-on-specific-child-elements/tree/naive-solution

Another Solution

Here's another solution to do, using useRef to get a reference to the component App then get all buttons inside it here's a code for this:

const appRef = useRef(null);
useEffect(() => {
  // here we got all rendered buttons inside the component
  const appButtons = appRef.current.getElementsByTagName("button"); 
  for (let btn of appButtons) {
    if (!isEnabled) btn.disabled = true;
    else if (isEnabled) btn.disabled = false;
  }
}, [isDisable]);
return (
  <div ref={appRef}> 
Enter fullscreen mode Exit fullscreen mode

In previous useEffect we got rendered buttons only not them all, and whenever a change happens the useEffect won't listen to it.

Hack it

We've to check if there's change in DOM, then call the body of the useEffect again, So we'll create two states to handle the tabs and subTabs changes, they're only elements to change the DOM, then pass their values to the dependancy array of the useEffect to listen the changes:

const [tabSelected, selectTab] = useState("1");
const [subTabSelected, selectSubTab] = useState("1");
.
.
.
useEffect(() => {
  // here we got all rendered buttons inside the component
  const appButtons = appRef.current.getElementsByTagName("button"); 
  for (let btn of appButtons) {
    if (!isEnabled) btn.disabled = true;
    else if (isEnabled) btn.disabled = false;
  }
}, [tabSelected, subTabSelected, isDisable]);
Enter fullscreen mode Exit fullscreen mode

Conclusion and corner cases

I think this solution is fit only when you have multiple nested element and you want to apply condition on them all without iterating over them manually.

Corner cases

  • If you want to exclude button from our previous app, you can add a class to it then check for it in the useEffect function.

  • If the component will re-render and get back to its initial state, then you have to return a function in the useEffect to undo whatever you did in the useEffect body it's componentWillUnmount in React Lifecycle.

Here's the final commit to the app https://github.com/zeyadetman/action-on-specific-child-elements/tree/final-commit

Top comments (3)

Collapse
 
ekeijl profile image
Edwin

I'm sorry, but this is not a great solution and I'll try to explain why:

Your mindset about state management in React seems off when I read this sentence:

If we have 10 nested buttons for example, it'll be hassle to iterate over them, also we may add more buttons or change the condition."

There should never be a hassle of iterating over child components to update them. That's the jQuery (or imperative) way of thinking. In React, you should think of a component's state as the single source of truth for that component. In our case, we want to share state between multiple components. The React documentation suggests that we lift our state up to a common ancestor component of all components that want to read the shared state. From this parent, you pass isDisabled as a prop.

This is actually what you call the naive solution. It might be naive, if you have to pass the isDisabled prop through multiple layers of components to reach your button component.

Sidenote: you should just use a regular attribute instead of spreading an object for a single prop:

// Instead of:
<Button {...(isDisable ? { disabled: true } : {})} />
// Use:
<Button disabled={isDisable} />

If you do need to drill props down multiple components, you can use other state management solutions. The easiest one is Context (there are others such as Redux and MobX, but the one you should use depends on the scope of your app). You can read those docs for a full guide, but the core is that your App defines a provider for the state value:

const App = ({children}) => {
   let [isDisabled, setDisabled] = useState(false);

   return <ButtonContext.Provider value={{isDisabled, setDisabled}}>{children}</App>;
};

Then in your consuming components, you can read the context value:

const Switch = () => {
   let {isDisabled, setDisabled} = useContext(ButtonContext);
   return <button onClick={() => setDisabled(!isDisabled)}>toggle</button>;
}

Then you only need to define your ButtonContext and import it in both components:

const ButtonContext = React.createContext();

Now, if you click the Switch, it will update the ButtonContext value in your App, and all Button components consuming the state will rerender with the updated value.

Boom. Done.

This way, there is no need for useRef, which you should really only use as a last resort for accessing the raw component or DOM node (e.g. when setting focus).

You also create tight coupling between your switch and your button components, because you update the buttons inside your switch. Even worse, it's tightly coupled to the implementation of your button, because you look up the DOM elements. The issue is that you need add a check for each additional element that wants to read the disabled state.

I hope you see the difference. This will become more obvious once your app starts scaling. Also read this article about thinking in React.

Collapse
 
zeyadetman profile image
Zeyad Etman

Thanks for your reply, But what I mean when you get into already written code, and there’s multiple nested ‘Button’ components and you don’t have the ability to change its internal code, as it’s been imported from external package.

Collapse
 
ekeijl profile image
Edwin

Aaaaaah yes, now I get it! Usually this happens if you want to use some NPM module that is not written in React, or a React component that is missing the props in its API to get it to do what you want.

In that case you want to create a wrapper React component that renders the 'external' component. From the wrapper component, you can use refs and useEffect, like in your original post to control the behaviour of the external component.

The point is that you keep the wrapper code that 'knows' about the internals as close as possible to the external package. That way you can still use my solution, where you pass props to the wrapper component and keep all your other components clean. 😀