Pass functional component from child to parent in React - javascript

Is it possible to pass a functional component from a child component to a parent component? I'm trying to do a dynamic modal that is displayed inside the parent but that the children can populate through a function from a provider, for example:
setModal(() => (
<div>content</div>)
)
And the parent receives this component:
const [modal, setModal] = useState(false)
const [modalContent, setModalContent] = useState<FunctionComponent>()
...
<Provider value={{
setModal: (content: FunctionComponent) => {
setModalContent(content); // This updates the state to hold a function component and to re-render
setModal(true); // This updates a state flag to show the overlay in which the modal is rendered
},
}}>
...
</Provider>
The content of the modal should be dynamic. I was trying to use the state of the component to hold the functional component but I don't think if that's possible or if it's a good practice.

If I understand your question correctly, you're still looking to pass a function from the parent to each child but each child should be able to change the state of a modal component that the parent also has ownership over.
For the above scenario this is something you can do:
const Provider = ({ children, updateModal }) => {
// With this, every child has the ability to call updateModal
return React.Children(children).map(child => cloneElement(child, { updateModal }));
};
const ModalComponent = ({ open, children }) => {
if (!open) return null;
return (
<dialog>
{children}
</dialog>
);
};
const ParentComponent = () => {
const [modal, setModal] = useState(false);
const [modalContent, setModalContent] = useState(null);
const updateModal = (content) => {
setModalContent(content);
setModal(true);
};
return (
<>
<Provider updateModal={updateModal}>
{...insert children here}
</Provider>
<ModalComponent open={modal}>
{modalContent}
</ModalComponent>
</>
);
};

Related

React sub-component expander and shared state

I'm wanting to create an expandable section with heading that when clicked toggles the expandable section to show/hide.
I have done this before with regular components etc, but this time I am trying to do this with sub-components and am coming a bit stuck with how to get the state working...
Should I be trying to pass the states into the sub-components directly in the main expander component, or should I be trying to use a context to share the state?
For context, I was reading this article which didn't delve into passing functions (helpful, I know).
App.js
const App = () => (
<div>
<Dropdown>
<Dropdown.Title>Dropdown One</Dropdown.Title>
<Dropdown.Body>Some content in the body</Dropdown.Body>
</Dropdown>
</div>
);
useExpandDropdown.js Custom hook
const useExpandDropdown = (initialState = false) => {
const [isExpanded, setIsExpanded] = useState(initialState);
const toggleExpand = () => setIsExpanded((prev) => !prev);
return [isExpanded, toggleExpand];
};
export default useExpandDropdown;
Expander.js
import useExpandDropdown from "../Hooks/useExpandDropdown";
import DropdownBody from "./DropdownBody";
import DropdownTitle from "./DropdownTitle";
const Dropdown = ({ children }) => {
const [isExpanded, toggleExpand] = useExpandDropdown();
return <div>{children}</div>;
};
Dropdown.Title = DropdownTitle;
Dropdown.Body = DropdownBody;
export default Dropdown;
ExpanderTitle.js
const DropdownTitle = ({ children }) => {
// I want to access the toggleExpand function in here
return <div>{children}</div>;
}
export default DropdownTitle;
ExpanderBody.js
const DropdownBody = ({ isExpanded, children }) => {
// I want to access the isExpanded state here
return <div>{children}</div>;
}
export default DropdownBody;
There are several ways to do it, and the right choice depends on the specifics—how your components are structured, what they look like and how you're using them.
But for most cases, I would outsource this kind of logic to a 3rd-party library so you can spend time maintaining your app instead. One choice is Headless UI and they have a component called Disclosure that you can use here.
import { Disclosure } from "#headlessui/react";
const App = () => (
<div>
<Disclosure>
<Disclosure.Button>
Dropdown One
</Disclosure.Button>
<Disclosure.Panel>
Some content in the body
</Disclosure.Panel>
</Disclosure>
</div>
);
As you can see, it's very simple, and depending on what exactly you're doing you might not need the Dropdown components at all.
Note that Disclosure.Button renders a button by default, which, depending on your environment, might come with some default styling you might not want. You should either style it or render something different than a button, e.g.:
<Disclosure.Button as={div}>
or
<Disclosure.Button as={DropdownTitle}>
Just remember to add a11y, since it's an interactive element.
One way is to use cloneElement to add the props (isExpanded or toggleExpand) to the children.
I'm using children[0] and children[1] to 'split' the title and body, this could be improved in a number of ways, like [ title, body ] = children if you're sure there are only 2 elements.
Example, press the title to toggle the body
const { useState } = React;
const useExpandDropdown = (initialState = false) => {
const [isExpanded, setIsExpanded] = useState(initialState);
return [isExpanded, () => setIsExpanded((prev) => !prev)];
};
const Dropdown = ({ children }) => {
const [isExpanded, toggleExpand] = useExpandDropdown();
return (
<div>
{React.cloneElement(children[0], { toggleExpand })}
{React.cloneElement(children[1], { isExpanded })}
</div>
)
};
const DropdownTitle = ({ children, toggleExpand }) => <div onClick={toggleExpand}>{children}</div>;
const DropdownBody = ({ children, isExpanded }) => <div>{'Body is: '}{isExpanded ? 'Visible' : 'Hidden'}</div>;
const Example = () => {
return (
<Dropdown>
<DropdownTitle>Title</DropdownTitle>
<DropdownBody>Some content in the body</DropdownBody>
</Dropdown>
)
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Here is another solution, using the render props pattern. In this approach, the state is managed by your main component, and passed to child components as props at render. This is a commonly used patterns in many libraries, e.g. Formik.
The advantage is complete flexibility—your API is open for extension in the future, as you can define the structure of your components without any restrictions. A disadvantage is that it's a little verbose and can result in prop drilling if you child components have several levels of nesting.
const { useState } = React;
const MyDisclosureTitle = ({
children,
onClick,
}) => {
const style = { all: "unset" };
return (
<button onClick={onClick} style={style} type="button">
{children}
</button>
);
};
const MyDisclosureBody = ({ children }) => {
return <div>{children}</div>;
};
const MyDisclosure = ({ children }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleExpanded = () => setIsExpanded((prev) => !prev);
const disclosureBag = {
isExpanded,
toggleExpanded,
};
return children(disclosureBag);
};
MyDisclosure.Title = MyDisclosureTitle;
MyDisclosure.Body = MyDisclosureBody;
const Example = () => {
return (
<MyDisclosure>
{({ isExpanded, toggleExpanded }) => (
<div>
<MyDisclosure.Title onClick={toggleExpanded}>
Dropdown One
</MyDisclosure.Title>
{isExpanded && (
<MyDisclosure.Body>Some content in the body</MyDisclosure.Body>
)}
</div>
)}
</MyDisclosure>
);
};
ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Here is a typescript example: https://codesandbox.io/s/react-disclosure-example-bqrtsk

Passing data from a child component to a parent component (React)

I have a child component that has some states which change in value when you check or un-check some boxes; I want to be able to pass those state values to the parent component. I've tried a few things on the internet but nothing seem to work.
I've tried to (Parent Component File):
const ParentComponent = () => {
const [ data, setData ] = useState();
const childToParent = (childData) => {
setData(childData);
};
return(
<Child childToParent={childToParent} />
);
}
and on the Child Component:
const ChildComponent = ({ childToParent }) => {
const [ childData, setChildData ] = useState(false);
const handleChange = () => {
setChildData(!childData);
childToParent(childData);
};
return(
<div>
<Checkbox value={childData} callback{() => handleChange()} />
</div>
);
}
Basically the problem is I dont get how to move data from a child component to a parent component.

Re-render a sibling component using hooks

I am new to react and I'm trying to get the one component to re-render from another component.
Here's my code:
const Parent = () => {
return (
<div>
<Child1 />
<Child2 />
</div>
)
}
What I intend to do is update Child1 when there is some trigger from Child2.
One way I can think of is to get the parent component to re-render so both Child1 and Child2 will be updated. I tried to do this by lifting the state but it doesn't seem to re-render each of the child components. Here's the code
const Parent = () => {
const [value, setValue] = useState('')
const handlePost = (newValue) => {
setValue(newValue)
}
return (
<div>
<Child1 />
<Child2 onPost={handlePost} />
</div>
)
}
const Child2 = (props) => {
// This function is executed when there is a trigger.
// In this case, when a post request is made to the server
const onPost() => {
props.handlePost('new value')
}
}
Edit:
The reason why the component(s) needs to be re-rendered is because they are making changes to the API and these changes need to be reflected on the screen. It has nothing to do with any state variables.
Your question is an XY problem. In the example given it does not make sense that Child1 rerenders cause there is no need for it. From the comments your real problem is that you update one API, which is supposed to change the response of another API. If you however already know how the response will change, and that it will change, this can be reflected in one state that changes for both API calls:
function useEntries() {
const [entries, setEntries] = useState([]);
useEffect(() => {
setEntries(getEntries());
}, []);
function addEntry(entry) {
postEntry(entry);
setEntries(prev => [...prev, entry]);
}
return { entries, addEntry };
}
function Parent() {
const { entries, addEntry } = useEntries();
return <>
<Entries entries={entries} />
<AddEntry addEntry={addEntry} />
</>;
}
From the comments in the post, it sounds like you have Child1 presenting results of a GET request (being done in Child1). Child2 can add or modify that state on the server with some kind of request and you want to trigger a re-render in order to make Child1 refresh the state.
The general problem is, that children should only re-render if props or their used contexts change. I see two options how to approach this:
Lift the handling of the requests up into the parent. Put the results of the request as props into the child component you want to refresh.
Make the sibling aware of the request having to reload by setting it to "dirty" in some way. Either through context or routing state around through the parent.
Usually it's best to go with option 1 if the components are not too deeply nested. It could look like this:
const Parent = () => {
const [posts, setPosts] = useState([]);
const fetchNewestPosts = useCallback(async () => {
const fetched = await fetchPosts();
setPosts(fetched);
}, [fetchPosts, setPosts]);
const handleSubmit = useCallback(async (event) => {
const newPost = getValuesFromSubmitEvent(event);
await sendNewPost(newPost);
// you could even set the posts here to what you think the
// send request will result in (see Jonas Wilms answer), like
// setPosts(posts => [newPost, ...posts]);
await fetchNewestPosts();
}, [fetchNewestPosts, getValuesFromSubmitEvent, sendNewPost]);
useEffect(() => {
fetchNewestPosts();
}, [fetchNewestPosts]);
return (
<div>
<Child1 posts={posts} />
<Child2 submitNewPost={submitNewPost} />
</div>
);
);
const Child1 = ({posts}) => {
return (
<ul>{posts.map(post => <li key={post.id}>post.title</li>)}</ul>
);
);
const Child2 = ({submitNewPost}) => {
return (
<form onSubmit={submitNewPost}>...</form>
);
);
As a nice side-effect, Child1 and Child2 now need a lot less logic and can be styled independently of the fetchPosts and sendNewPost functions.
Ciao, lets say that Child1 must be re-rendered on handlePost. Your parent component will be:
const Parent= () => {
const [value, setValue] = useState('')
const [rerender, setrerender] = useState(false)
const handlePost = (newValue) => {
setValue(newValue);
let setrerender_temp = rerender;
setrerender(!setrerender_temp);
}
return (
<div>
<Child1 rerender={rerender} />
<Child2 onPost={handlePost} />
</div>
)
}
Then, in your Child1 component:
import React, { useReducer, useEffect } from 'react';
...
export default function Child1(props) {
const [,forceRender] = useReducer((s) => s+1, 0);
useEffect(() => forceRender(), [props.rerender]);
...
}

React conditionally render component using an outside function

I'm working on a modal function in an application. Since the app has different modals, I have a function which handles the open & close state of various windows:
OpenItem.jsx
const OpenItem = ({ toggle, content }) => {
const [isShown, setIsShown] = useState(false);
const hide = () => setIsShown(false);
const show = () => setIsShown(true);
return (
<>
{toggle(show)}
{isShown && content(hide)}
</>
);
};
export default OpenItem;
Header.jsx
Now in my main component, I want to to use this function with another component:
const Header = () => {
return (
<div>
<OpenItem
toggle={(show) => <Button onClick={show}>icon</Button>}
content={(hide) => (
// Component to hide:
<ComponentToShowOrHide onClick={hide} />
)}
/>
</div>
);
};
export default Header;
This works fine, except that instead of having the {hide} function as a part of the imported component, I want to toggle the view in <Button onClick={show}>icon</Button>
My idea is to conditionally render the show or hide in the button instead of rendering it in the component, but I'm not quite sure how to do that since I haven't used an outside function to control a function in a component.
Simply write a function that toggles the state rather than sets it to a value.
const OpenItem = ({ toggle, content }) => {
const [isShown, setIsShown] = useState(false);
return (
<>
{toggle(() => setIsShown(prevState => !prevState))}
</>
);
};
export default OpenItem;

How to simulate parent component state while testing with jest

I have this component, props are passed from parent component. Ingredients and activeIngredients are stored in the state of parent component.
export const IngredientsBox = ({
ingredients = [],
activeIngredients = [],
onAddIngredientHandler,
onRemoveIngredientHandler,
onResetIngredientsHandler
}) => {
return (
<Div>
{ingredients.map((name) => {
return (
<IngredientButton
name={name}
key={name}
isActive={activeIngredients.includes(name)}
onAddIngredientHandler={onAddIngredientHandler}
onRemoveIngredientHandler={onRemoveIngredientHandler}
/>
);
})}
<ResetButton onResetIngredientsHandler={onResetIngredientsHandler}></ResetButton>
</Div>
);
};
export const IngredientButton = ({ name, isActive, onAddIngredientHandler, onRemoveIngredientHandler }) => {
const onClick = isActive ? onRemoveIngredientHandler : onAddIngredientHandler;
return (
<Button active={isActive} onClick={() => onClick(name)}>
{name}
</Button>
);
};
What I wanna do is to test this component in isolation, but can't figure out how to imitate parent component state to change dynamically, after every method call.
import React from 'react';
import {IngredientsBox} from './IngredientsBox';
import renderer from 'react-test-renderer';
// ingredients and activeIngredients are supposed to imitate parent component state
let ingredients = ['sugar', 'honey', 'mustard', 'watermelon'];
let activeIngredients = [];
const onAddIngredientHandler = (name) => {
activeIngredients = [...activeIngredients, name]
}
const onRemoveIngredientHandler = (name) => {
activeIngredients = activeIngredients.filter((value) => value !== name)
};
test('Button toggle the class on click', () => {
const component = renderer.create(
<IngredientsBox
ingredients={ingredients}
activeIngredients = {activeIngredients}
isActive = {activeIngredients.includes(name)}
onAddIngredientHandler = {onAddIngredientHandler}
onRemoveIngredientHandler = {onRemoveIngredientHandler}
/>
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
tree.children[0].props.onClick(); // I expect this to add this element to activeIngredients and it ofcourse works.
tree = component.toJSON();
expect(tree).toMatchSnapshot();
tree.children[0].props.onClick(); // I expect this to remove this element from activeRecipes, it doesn't work, it adds it one more time, and so on. I understand this behaviour is because onClick method was assigned at the beginning and it doesn't change.
tree = component.toJSON();
expect(tree).toMatchSnapshot()
})
Is there any way to make it behave like react component with render() method? So it rerender with fresh state each time?

Categories