damtypo
damtypo

Reputation: 160

How to communicate between React components which do not share a parent?

I am adding React to an already existing front end and am unsure how to communicate data between components.

I have a basic text Input component and a Span component, mounted separately. When the user types into the input, I want the text of the span to change to what is input.

Previously I would start a React project from scratch and so have the Input and Span share an App component as a parent. I'd use a prop function to lift the text state from the Input to the App and pass it down the value to the Span as a prop. But from scratch is not an option here.

I've considered:

  1. Redux etc. As I'm introducing React piece by piece to this project and some team members have no React experience, I want to avoid using Redux or other state management libraries until very necessary, and it seems overkill for this simple case.

  2. React Context API. This doesn't seem correct either, as my understanding was that context API should be kept for global data like "current authenticated user, theme, or preferred language" shared over many components, not just for sharing state between 2 components.

  3. UseEffect hook. Using this hook to set the inner HTML of the Span component i.e

function Input() {
    const inputProps = useInput("");
    useEffect(() => {
        document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;

    })
    return (
        <div>
            <h3>Name this page</h3>
            <input 
                placeholder="Type here"
                {...inputProps} 
            />
        </div>
    );
}

Which sort of negates the whole point of using React for the Span?

I've gone with the UseEffect hook for now but haven't found any clear answers in the React docs or elsewhere online so any advice would be helpful.

Thanks.

Input.jsx

import React, { useState, useEffect } from 'react';

function useInput(defaultValue) {
    const [value, setValue] = useState(defaultValue);

    function onChange(e) {
        setValue(e.target.value);
    }

    return {
        value, 
        onChange
    }
}


function Input() {
    const inputProps = useInput("");
    useEffect(() => {
        document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;

    })
    return (
        <div>
            <h3>React asks what shall we name this product?</h3>
            <input 
                placeholder="Type here"
                {...inputProps} 
            />
        </div>
    );
}


export default Input;

PageTitle.jsx

import React from 'react';


function PageTitle(props) {
    var title = "Welcome!"
    return (    
        <span>{props.title}</span>
    )
}

;
export default PageTitle

Index.js

// Imports

const Main = () => (
    <Input />
);

ReactDOM.render(
    <Main />,
    document.getElementById('react-app')
);

ReactDOM.render(
    <PageTitle title="Welcome"/>,
    document.getElementsByClassName('page-title')[0]
);

Upvotes: 1

Views: 1199

Answers (2)

Neal Burns
Neal Burns

Reputation: 849

In React, data is supposed to flow in only one direction, from parent component to child component. Without getting into context/redux, this means keeping common state in a common ancestor of the components that need it and passing it down through props.

Your useEffect() idea isn't horrible as a kind of ad hoc solution, but I would not make PageTitle a react component, because setting the value imperatively from another component really breaks the react model.

I've used useEffect() to set things on elements that aren't in react, like the document title and body classes, as in the following code:

const siteVersion = /*value from somewhere else*/;
//...

useEffect(() => {

    //put a class on body that identifies the site version
    const $ = window.jQuery;
    if(siteVersion && !$('body').hasClass(`site-version-${siteVersion}`)) {
        $('body').addClass(`site-version-${siteVersion}`);
    }

    document.title = `Current Site: ${siteVersion}`;
    
}, [siteVersion]);

In your case, you can treat the span in a similar way, as something outside the scope of react.

Note that the second argument to useEffect() is a list of dependencies, so that useEffect() only runs whenever one or more changes.

Another side issue is that you need to guard against XSS (cross site scripting) attacks in code like this:

//setting innerHTML to an unencoded user value is dangerous
document.getElementsByClassName('page-title')[0].innerHTML = inputProps.value;

Edit:

If you want to be even more tidy and react-y, you could pass a function to your input component that sets the PageTitle:

const setPageTitle = (newTitle) => {
    //TODO: fix XSS problem
    document.getElementsByClassName('page-title')[0].innerHTML = newTitle;
};

ReactDOM.render(
    <Main setPageTitle={setPageTitle} />,
    document.getElementById('react-app')
);

//inside Main:
function Input({setPageTitle}) {
    const inputProps = useInput("");
    useEffect(() => {
        setPageTitle(inputProps.value);
    })
    return (
        <div>
            <h3>React asks what shall we name this product?</h3>
            <input 
                placeholder="Type here"
                {...inputProps} 
            />
        </div>
    );
}

Upvotes: 1

jrana
jrana

Reputation: 16

You can create a HOC or use useContext hook instead

Upvotes: 0

Related Questions