Problem: cannot get ref to update from {current: null} to the actual ref on the component.
What i want to happen: {current: null}, as i understand it, should update to include the div that ref is on in order to be able to click ouside of it (eventually to close it). 9 understand that it does not update on first render, but it does not ever update. It does run twice on page load, both returning current: null.
What i tried: i have followed all the SO advice to use useEffect and then finally separating it into this function which appears to be the most appropriate and up to date method to do this. It just never updates current.
function useOutsideAlerter(ref) {
useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
console.log(ref);
} else {
console.log("else", ref);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
}
export const Modal = (props) => {
const [showModal, setShowModal] = useState(props.showModal);
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef);
return (
<Layout>
<ModalOuter
showModal={showModal || props.showModal}
id={styles["modalOuter"]}
handleClose={props.handleClose}
>
<ModalInner
ref={wrapperRef}
handleClose={props.handleClose}
>
<Layout display="flex" flexDirection="column">
<Layout display="flex" flexDirection="column">
<ModalTitle title={props.title} />
</Layout>
<HR />
<Layout display="flex" flexDirection="column">
<ModalBody body={props.body} />
</Layout>
</Layout>
</ModalInner>
</ModalOuter>
</Layout>
);
};
ModalInner
export const ModalInner = (props) => {
return (
<Layout
id={props.id}
ref={props.ref}
display="flex"
justifyContent="center"
alignItems="center"
padding="2rem"
margin="2rem"
backgroundColor="white"
>
{props.children}
</Layout>
);
};
Layout Component
export const Layout = (props) => {
return (
<div
id={props.id}
ref={props.ref}
...
Issue
In React, there are a few special "props", ref and key are a couple of them. I put quotes around props because while they are passed as props, they are not passed on to or accessible on the props object in children components.
Solution
Use React.forwardRef to forward any passed React refs to functional components and expose them in children components.
export const ModalInner = React.forwardRef((props, ref) => { // <-- access ref
return (
<Layout
id={props.id}
ref={ref} // <-- pass ref *
display="flex"
justifyContent="center"
alignItems="center"
padding="2rem"
margin="2rem"
borderRadius="5px"
backgroundColor="white"
border={`1px solid ${Color.LightGray}`}
boxShadow={`0rem 0rem 1rem white`}
>
{props.children}
</Layout>
);
});
* Note: The Layout and children components will similarly need to forward the ref until you get to where it's actually attached to a DOMNode.
An alternative solution is to pass the ref as a normal prop.
<ModalInner
wrapperRef={wrapperRef}
handleClose={props.handleClose}
>
...
export const ModalInner = (props) => {
return (
<Layout
id={props.id}
wrapperRef={props. wrapperRef} // <-- pass wrapperRef prop
display="flex"
justifyContent="center"
alignItems="center"
padding="2rem"
margin="2rem"
borderRadius="5px"
backgroundColor="white"
border={`1px solid ${Color.LightGray}`}
boxShadow={`0rem 0rem 1rem white`}
>
{props.children}
</Layout>
);
};
Similarly, you need to drill the wrapperRef prop on through to children until you get to the actual DOMNode where you attach the ref.
Example
<div ref={props.wrapperRef> .... </div>
You may also find Refs and the DOM docs useful for working with React refs.
Related
I have a dynamic array (state) of React components – and each components has an entry-animation on mount. But every time a component is added to the array all the components re-renders – which also triggers the entry-animation for all components...
My parent page looks something like this:
export default function Home({ projects }) {
const [components, setComponents] = useState([]);
const createComponent = (project) => {
const id = uniqid();
const temp = <Project block={project} key={id} />;
setOpenBlocks((prevState) => [temp, ...prevState]);
};
return (
<>
//Small images / Create component on click:
<div>
{projects.map((project, i) =>
<div key={project.page.id}>
<Image src alt
onClick={() => createComponent(project)}
/>
</div>
})}
</div>
//Big images / render array of components:
<div>
{components &&
components.map((block, i) => <Fragment key={i}>{component}</Fragment>)}
</div>
</>
);
}
And my 'Project' (child) component looks like this:
export default function Project({ project }) {
const [loaded, setLoaded] = useState(0);
return (
<AnimatePresence>
{project && (
<motion.figure
initial={{ width: 0 }}
animate={{ width: "100%" }}
style={{ opacity: loaded }}
>
<img
onLoad={() => setLoaded(1)}
/>
</motion.figure>
)}
</AnimatePresence>
)
}
So the entry-animation is made via the framer-motion AnimatePresence component, as well as the onLoad function changing opacity from 0 to 1. Both of them re-triggers when updating the array. And I need only the component that was just added to animate!
The child components props will not change once it is rendered.
I've tried wrapping the child component in 'memo', and tried updating the array with useCallback. Inserting the array like this somehow seemed to work (but I don't think it should?):
<div>
{components}
</div>
All input is welcome, thanks!
I have written page that uses Tabs and Tab Panel. I'm writing UI tests using react-testing library. The theme has been modified into custom theme that is imported at the root of the Next.js app. I'm using page that uses a component with form for the Tab. In the listening it's <MyComponent />. The component inside has Select component used from ChakraUI. Other inputs don't affect on the error that appears.
Libraries:
ChakraUI 2.8
react-hook-form
Next.js 12
The error that appears is
Cannot read properties of undefined (reading '_focus')
TypeError: Cannot read properties of undefined (reading '_focus')
at /path/to/project/node_modules/#chakra-ui/select/dist/index.cjs.js:102:22
My Page in a nutchell looks like
const MyPage () => {
const instrumentInfoTab = React.useRef() as React.MutableRefObject<HTMLInputElement>;
return (
<Tabs>
<TabList><Tab>...</Tab</TabLists>
</Tabs>
<TabPanels>
<TabPanel>
<MyComponent updateTransactionState={/*some function to handle state*/} nextTabRef={instrumentInfoTab} />
</TabPanel>
</TabPanels>
<Tab
)
}
MyComponent
interface IInstrumentInfoPanelProps {
nextTabRef: React.MutableRefObject<HTMLInputElement>;
updateTransactionState: (data: InstrumentInfoInput) => void;
}
const MyComponent = (props: IInstrumentInfoPanelProps) => {
const { nextTabRef, updateTransactionState } = props;
const textColor = useColorModeValue('secondaryGray.900', 'white');
const methods = useForm<InstrumentInfoInput>({
resolver: zodResolver(instrumentInfoSchema)
});
const { handleSubmit, register } = methods;
const onSubmit = (data: InstrumentInfoInput) => {
updateTransactionState(data);
nextTabRef.current.click();
};
return (
<TabPanel>
<CustomCard>
<Text>
Instrument Info
</Text>
<Flex>
<form onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...methods}>
<SimpleGrid>
<Stack>
<Flex direction="column" mb="34px">
<FormLabel
ms="10px"
htmlFor="transactionType"
fontSize="sm"
color={textColor}
fontWeight="bold"
_hover={{ cursor: 'pointer' }}>
Transaction type*
</FormLabel>
<Select
{...register('transactionType')}
id="transactionType"
variant="main"
defaultValue="buy">
<option value="buy">BUY</option>
<option value="sell">SELL</option>
</Select>
</Flex>
</Stack>
<Stack>
<InputField
id="accountName"
name="accountName"
placeholder="eg. Coinbase"
label="Account Name*"
data-testid="instrumentInfoPanel-accountName"
/>
</Stack>
</SimpleGrid>
<Flex justify="space-between" mt="24px">
<Button
type="submit">
Next
</Button>
</Flex>
</FormProvider>
</form>
</Flex>
</CustomCard>
</TabPanel>
);
};
export default MyComponent;
My Test looks like. The error appears in render function
jest.mock('next/link', () => {
return ({ children }) => {
return children;
};
});
interface IWrapedComponent {
children?: JSX.Element;
}
const test = (ref: MutableRefObject<HTMLElement | null>): void => {
if (ref.current) ref.current.focus();
};
const WrappedComponent = (props: IWrapedComponent) => {
const { children } = props;
const ref = React.useRef() as React.MutableRefObject<HTMLInputElement>;
useEffect(() => {
test(ref);
}, []);
return (
<Tabs>
<TabList>
<Tab ref={ref}></Tab>
</TabList>
<TabPanels>
<MyComponent />
</TabPanels>
</Tabs>
);
};
describe('Instrument Info Panel', () => {
it('should render inputs for instrument info', () => {
render(
<QueryClientProvider client={new QueryClient()}>
<WrappedComponent />
</QueryClientProvider>
);
});
});
In order to debug an issue I have tried to remove other inputs from form and it worked when there were different input types than Select from ChakraUI.
Goal: I should display the specific contents of a specific button after one of three buttons was clicked. Then, after the specific button is clicked, all three buttons should be hidden and replaced with the contents of the clicked specific button.
Issue: I tried passing props and using if-else statement in terms of conditional rendering but I am having trouble figuring out how to properly state a condition for the functionality to work since the remaining if else statements are ignored. Only the Beef button is working but the rest of the buttons are not.
Source code:
import * as React from "react";
import { Stack } from '#mui/material';
import FoodTraysItemButton from "./FoodTraysItemButton";
import PastaNoodlesButtonsFT from "./foodTraysPages/PastaNoodlesButtonsFT";
import DessertsButtonsFT from "./foodTraysPages/DessertsButtonsFT";
import BeefButtonsFT from "./foodTraysPages/BeefButtonsFT";
import { useState } from "react";
function preventDefault(event) {
event.preventDefault();
}
export default function FoodTraysButtons(props) {
const [myBoolBeef, setmyBoolBeef] = useState(true);
const [myBoolDesserts, setmyBoolDesserts] = useState(true);
const [myBoolPastaNoodles, setmyBoolPastaNoodles] = useState(true);
function toggleBoolBeef() {
setmyBoolBeef(!myBoolBeef);
}
function toggleBoolDesserts() {
setmyBoolDesserts(!myBoolDesserts);
}
function toggleBoolPastaNoodles() {
setmyBoolPastaNoodles(!myBoolPastaNoodles);
}
return (
// stuck here: (I plan to use multiple separate if else statements to work the functionality out but it doesn't work)
<React.Fragment>
{(() => {
// only works here
if (myBoolBeef) {
return (<Landing toggleBoolBeef={toggleBoolBeef} />);
} else{
return <BeefFT/>;
}
// these are ignored:
if (myBoolDesserts) {
return (<Landing toggleBoolDesserts={toggleBoolDesserts} />);
} else{
return <DessertsFT/>;
}
if (myBoolPastaNoodles) {
return (<Landing toggleBoolPastaNoodles={toggleBoolPastaNoodles} />);
} else{
return <PastaNoodlesFT/>;
}
})()}
</React.Fragment>
);
}
function Landing(props) {
return (
<div>
<Stack spacing={0} direction="row" sx={{ mb: 4.5 }}>
<FoodTraysItemButton
title="Beef"
onClick={props.toggleBoolBeef}
/>
<FoodTraysItemButton
title="Desserts"
onClick={props.toggleBoolDesserts}
/>
<FoodTraysItemButton title="Pasta/Noodles" onClick={props.toggleBoolPastaNoodles} />
</Stack>
</div>
);
}
function BeefFT() {
return (
<div>
<BeefButtonsFT />
</div>
);
}
function DessertsFT() {
return (
<div>
<DessertsButtonsFT />
</div>
);
}
function PastaNoodlesFT() {
return (
<div>
<PastaNoodlesButtonsFT />
</div>
);
}
Full source codes in Codesandbox: https://codesandbox.io/s/show-hide-buttons-ralph-ecv9g2?file=/src/FoodTraysButtons.jsx:773-815
How it should look like:
Beef button:
Desserts button:
Pasta Noodles button:
In what way should I implement this in order to achieve its functionality?
Your responses would be highly appreciated as I am exploring MUI and React at the moment. It would be a really big help for my project. Thank you very much!!!
Update FoodTraysButtons to hold a single state, selection that is then used to conditionally render the Landing component or any of BeefFT, DessertsFT, or PastaNoodlesFT component.
export default function FoodTraysButtons(props) {
const [selection, setSelection] = useState();
const selectHandler = (selection) => setSelection(selection);
return (
<React.Fragment>
{!selection && <Landing onSelect={selectHandler} />}
{selection === "beef" && <BeefFT />}
{selection === "dessets" && <DessertsFT />}
{selection === "pastaNoodles" && <PastaNoodlesFT />}
</React.Fragment>
);
}
Update the Landing component to take a single onSelect prop callback.
function Landing({ onSelect }) {
const selectHandler = (selection) => () => onSelect(selection);
return (
<div>
<Stack spacing={0} direction="row" sx={{ mb: 4.5 }}>
<FoodTraysItemButton title="Beef" onClick={selectHandler("beef")} />
<FoodTraysItemButton
title="Desserts"
onClick={selectHandler("desserts")}
/>
<FoodTraysItemButton
title="Pasta/Noodles"
onClick={selectHandler("pastaNoodles")}
/>
</Stack>
</div>
);
}
You need a switch case block instead of multiple boolean value state. Consider this way of structuring your code:
const menuState = {
NONE: "none",
BEEF: "beef",
DESSERTS: "desserts",
PASTA: "pasta"
};
export default function FoodTraysButtons(props) {
const [selectedMenu, setSelectedMenu] = useState(menuState.NONE);
const renderMenu = () => {
switch (selectedMenu) {
case menuState.BEEF:
return <BeefFT />;
case menuState.DESSERTS:
return <DessertsFT />;
case menuState.PASTA:
return <PastaNoodlesFT />;
case menuState.NONE:
default:
return null;
}
};
return (
<React.Fragment>
{selectedMenu === menuState.NONE && (
<Landing setSelectedMenu={setSelectedMenu} />
)}
{renderMenu()}
</React.Fragment>
);
}
function Landing(props) {
return (
<div>
<Stack spacing={0} direction="row" sx={{ mb: 4.5 }}>
<FoodTraysItemButton
title="Beef"
onClick={() => props.setSelectedMenu(menuState.BEEF)}
/>
<FoodTraysItemButton
title="Desserts"
onClick={() => props.setSelectedMenu(menuState.DESSERTS)}
/>
<FoodTraysItemButton
title="Pasta/Noodles"
onClick={() => props.setSelectedMenu(menuState.PASTA)}
/>
</Stack>
</div>
);
}
Working Demo:
NOTE: If you want to always show the button menu then remove the selectedMenu === menuState.NONE wrapper condition.
This is either very simple or I am doing it completely wrong. I am a novice so please advise.
I am trying to show different components inside different tabs using Material UI using array map. The tabs are showing fine but the components do not render. Basically if the array label is 'Welcome', the tab name should be 'Welcome' and the Welcome component should show up and so on. Please help!
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{fetchedCategories.map((category) => (
<Tab key={category.label} label={category.label} />
))}
</Tabs>
</Box>
{fetchedCategories.map((category, index) => {
const Component=myComponents[category.label];
})}
{fetchedCategories.map((category, index) => (
<TabPanel key={category.label} value={value} index={index}>
<Component label={category.label} />
</TabPanel>
))}
</Box>
);
Here is my props & Component function:
interface ComponentProps {
label: string;
value?: number;
}
function Component (props: ComponentProps)
{
const {label, value} = props;
return myComponents[label];
}
const myComponents = {
'Welcome': Welcome,
'Salad/Soup': Welcome
}
Try something like:
function Component({ label, value }: ComponentProps) {
const [Comp, setComponent] = useState(<div />);
React.useEffect(() => {
const LabelComp = myComponents[label];
if (label && LabelComp) {
setComponent(<LabelComp value={value} />); // <-- if you want to pass value to you component
}
}, [value, label]);
return Comp;
}
And you will use it like:
const App = () => {
return <Component label={"myComponentLabel"} value={"some value"} />;
};
I am building a an app that has a map and side-tabs.
Every time a user clicks on the map a marker appears and the coordinates are stored in a used state array.
I want every time a new marker appears to show it as a list or an accordion item in my side-tabs.
My side-tabs component and my addmarker component have the App as a parent.
How can I pass the usestate array from my addmarker component to my sidebar component every time I click on the map ?
ADD MARKER COMPONENT
function AddMarker(callbackFunction){
const [coord, setPosition] = useState([]);
const map = useMapEvents({
click: (e) => {
setPosition([...coord,e.latlng])
const mark = e
//console.log(mark)
//setInfo(`${e.latlng}`)
},
SIDE-BAR COMPONENT
export default function VerticalTabs() {
const [value, setValue] = React.useState(0);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<Box
sx={{ flexGrow: 1, bgcolor: 'background.paper', display: 'flex', height: 224 }}
>
<Tabs
orientation="vertical"
value={value}
onChange={handleChange}
aria-label="Vertical tabs"
sx={{ borderRight: 1, borderColor: 'divider' }}
>
<Tab label="Waypoints" {...a11yProps(0)} />
<Tab label="Sorting" {...a11yProps(1)} />
</Tabs>
<TabPanel value={value} index={0}>
</TabPanel>
<TabPanel value={value} index={1}>
Sorting
</TabPanel>
</Box>
);
}
APP.JS
function App() {
return (
<div className="App" >
<Sidetabs/>
<MapContainer center={[40.44695, -345.23437]} zoom={3}>
..............
<AddMarker />
</MapContainer>
</div>
)
}
Here, always lifting one step up, helps everytime, i have lifted your state up, and now both children have access to it, https://reactjs.org/docs/lifting-state-up.html
the similar implementation could be look like this,
as both siblings are nothing but children to parent App
function App() {
const [coord, setPosition] = useState([]);
return (
<div className="App" >
<Sidetabs coord={coord} setPosition={setPosition}/>
<MapContainer center={[40.44695, -345.23437]} zoom={3}>
<AddMarker coord={coord} setPosition={setPosition}/>
</MapContainer>
</div>
)
}
then extract out using props,
function AddMarker(props){
const {coord, setPosition} = props;
}