React conditionally render component using an outside function - javascript

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;

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

Pass Data from Parent To Child Component in ReactJs

In App.Js file I'm passing "This data is coming from Parent" to Child.Js
But here I want to pass this on button through onClick not dataParentToChild={data}. Just Like Child.Js file working in onClick={handleChildToProp}
enter the link description here
App.Js
function App(props) {
// Child To Parent
const [word, setWord] = useState("");
const handleChildToParent = (words) => setWord(words);
// Parent To Child
const data = "This data is coming from Parent";
return (
<>
<h1>"Parent Data"</h1>
<h2>{word}</h2>
<Child
// Without button Working Fine
dataParentToChild={data}
// With button Working Fine
dataChildToParent={handleChildToParent}
/>
</>
);
}
Child.Js
const Child = (props) => {
// Parent To Child
const handleChildToProp = () => {
props.dataChildToParent("This data is comming from Child");
};
return (
<>
<h1>Child Components</h1>
<h2>{props.dataParentToChild}</h2>
<button onClick={handleChildToProp}>Data Child To Parent</button>
</>
);
};
You need to make the button onClick update a state to trigger a re-render of dataParenToChild, like so:
function App(props) {
// Child To Parent
const [word, setWord] = useState('');
const [parentToChild, setParentToChild] = useState('');
const handleChildToParent = (words) => setWord(words);
// Parent To Child
const handleParentToChild = () => {
setParentToChild('This data is coming from Parent');
};
return (
<>
<h1>"Parent Data"</h1>
<h2>{word}</h2>
<button onClick={handleParentToChild}>Data Parent To Child</button>
<Child
// Without button Working Fine
dataParentToChild={parentToChild}
// With button Working Fine
dataChildToParent={handleChildToParent}
/>
</>
);
}
Working solution

Pass functional component from child to parent in React

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

React - same callback for each component in array

Lets say I have a components array in my React app:
const deleteProject = useCallback(project => {
// something
}, []);
return (
projects.map(p => (
<button onClick={() => deleteProject(p)}>Delete</button>
);
);
Is there any way I could use just deleteProject function without wrapping it into separate callbacks i.e. {} => {} for each component? This is for performance purposes. I mean something like:
<button onClick={deleteProject}>Delete</button>
And then in deleteProject somehow I'd need to determine which project to delete, but how? It only takes click event as argument
If you have a long projects list and see performance issues you could define DeleteButton component to avoid button re-rendering
const DeleteButton = ({project, deleteProject}) => {
const onClick = useCallback(
() => deleteProject(project),
[project, deleteProject],
);
return <button onClick={onClick}>Delete</button>
}
const YourComponent = ({projects, deleteProject}) => (
<>
{projects.map(project => <DeleteButton {...{project, deleteProject}}/>)
</>
)
You can achieve by assigning project identifier to button as below
const deleteProject = event => {
projectId = event.target.id;
// Delete project here using id
}
return (
projects.map(p => (
<button id={p.id} onClick={deleteProject}>Delete</button>
);

Using React Context as part of useEffect dependency array for simple toast notification system

I'm building a simple toast notification system using React Context. Here is a link to a simplified but fully working example which shows the problem https://codesandbox.io/s/currying-dust-kw00n.
My page component is wrapped in a HOC to give me the ability to add, remove and removeAll toasts programatically inside of this page. The demo has a button to add a toast notification and a button to change the activeStep (imagine this is a multi-step form). When the activeStep is changed I want all toasts to be removed.
Initially I did this using the following...
useEffect(() => {
toastManager.removeAll();
}, [activeStep]);
...this worked as I expected, but there is a react-hooks/exhaustive-deps ESLint warning because toastManager is not in the dependency array. Adding toastManager to the array resulted in the toasts being removed as soon as they were added.
I thought I could have fixed that using useCallback...
const stableToastManager = useCallback(toastManager, []);
useEffect(() => {
stableToastManager.removeAll();
}, [activeStep, stableToastManager]);
...however, not only does this not work but I would rather fix the issue at the source so I don't need to do this every time I want this kind of functionality, as it is likely to be used in many places.
This is where I am stuck. I'm unsure as to how to change my Context so that I don't need add additional logic in the components that are being wrapped by the HOC.
export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = useState([]);
const add = (content, options) => {
// We use the content as the id as it prevents the same toast
// being added multiple times
const toast = { content, id: content, ...options };
setToasts([...toasts, toast]);
};
const remove = id => {
const newToasts = toasts.filter(t => t.id !== id);
setToasts(newToasts);
};
const removeAll = () => {
if (toasts.length > 0) {
setToasts([]);
}
};
return (
<ToastContext.Provider value={{ add, remove, removeAll }}>
{children}
<div
style={{
position: `fixed`,
top: `10px`,
right: `10px`,
display: `flex`,
flexDirection: `column`
}}
>
{toasts.map(({ content, id, ...rest }) => {
return (
<button onClick={() => remove(id)} {...rest}>
{content}
</button>
);
})}
</div>
</ToastContext.Provider>
);
};
export const withToastManager = Component => props => {
return (
<ToastContext.Consumer>
{context => {
return <Component toastManager={context} {...props} />;
}}
</ToastContext.Consumer>
);
};
If you want to "Fix it from the core", you need to fix ToastProvider:
const add = useCallback((content, options) => {
const toast = { content, id: content, ...options };
setToasts(pToasts => [...pToasts, toast]);
}, []);
const remove = useCallback(id => {
setToasts(p => p.filter(t => t.id !== id));
}, []);
const removeAll = useCallback(() => {
setToasts(p => (p.length > 0 ? [] : p));
}, []);
const store = useMemo(() => ({ add, remove, removeAll }), [
add,
remove,
removeAll
]);
Then, the useEffect will work as expected, as the problem was that you re-initialized the ToastProvider functionality on every render when it needs to be a singleton.
useEffect(() => {
toastManager.removeAll();
}, [activeStep, toastManager]);
Moreover, I would recommend to add a custom hook feature as the default use case, and providing wrapper only for class components.
In other words, do not use wrapper (withToastManager) on functional components, use it for classes, as it is considered an anti-pattern, you got useContext for it, so your library should expose it.
// # toastContext.js
export const useToast = () => {
const context = useContext(ToastContext);
return context;
};
// # page.js
import React, { useState, useEffect } from 'react';
import { useToast } from './toastContext';
const Page = () => {
const [activeStep, setActiveStep] = useState(1);
const { removeAll, add } = useToast();
useEffect(() => {
removeAll();
}, [activeStep, removeAll]);
return (
<div>
<h1>Page {activeStep}</h1>
<button
onClick={() => {
add(`Toast at ${Date.now()}!`);
}}
>
Add Toast
</button>
<button
onClick={() => {
setActiveStep(activeStep + 1);
}}
>
Change Step
</button>
</div>
);
};
export default Page;

Categories