I am new with React development and I wonder if this is considered an anti-pattern to pass down setState hook as props.
My code looks like this:
const App = () => {
const [value, setValue] = useState('')
return (
<Component value={value} helpers={{ setValue }} />
)
}
const Component = (props) => {
return <Component2 {...props} />
}
const Component2 = (props) => {
return <Input {...props} />
}
const Input = (props) => {
const handleChange = (e) => props.helpers.setValue(e.target.value)
return <input onChange={handleChange} {...props} />
}
I am working with React-Hook-Form and I need to declare the state at the top level but I still want my handleChange function to be at the component level. However, I don't see this pattern on example codes.
Is it an anti-pattern?
Related
I have this simplified structure:
<Page>
<Modal>
<Form />
</Modal>
</Page>
All of these are functional components.
And in <Modal /> I have a close function that looks like this:
const close = () => {
// apply a CSS class - so the modal disappears animatedly ..
// then use setTimeout() to completely remove the modal after the animation ends ..
}
Do you have an idea how the <Page /> component can call the <Modal /> close method? And the page has to do it because this is where I'm doing the call to API with the data from the form, and so if all is OK with API request - close the modal.
(The <Form /> handles only the form validation but then passes the data to <Page /> where all the business logic happens.)
PS: The project uses Typescript ... so I have to deal with types as well :(
I look into your problem. I think my example should clarify your problem. Let me know if you have any questions.
import { ReactNode, useCallback, useEffect, useState } from 'react'
import { render } from 'react-dom'
function App() {
return (
<div>
<Page />
</div>
)
}
function Page() {
const [isModalOpen, setModalOpen] = useState(false)
const handleFormSubmit = useCallback((formValues: FormValues) => {
console.log({ formValues })
setModalOpen(false)
}, [])
return (
<div>
<button onClick={() => setModalOpen(!isModalOpen)}>Toggle modal</button>
<Modal isOpen={isModalOpen}>
<Form onSubmit={handleFormSubmit} />
</Modal>
</div>
)
}
interface ModalProps {
isOpen: boolean
children: ReactNode
}
function Modal(props: ModalProps) {
const [isOpen, setIsOpen] = useState(false)
const close = () => {
setIsOpen(false)
}
const open = () => {
setIsOpen(true)
}
useEffect(() => {
if (!props.isOpen && isOpen) close()
if (props.isOpen && !isOpen) open()
}, [props.isOpen])
if (!isOpen) return null
return <div className="modal">{props.children}</div>
}
interface FormProps {
onSubmit: (formValues: FormValues) => void
}
interface FormValues {
username: string
password: string
}
function Form(props: FormProps) {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
return (
<form
onSubmit={e => {
e.preventDefault()
props.onSubmit({
username,
password
})
}}
>
<input
type="text"
placeholder="username"
onChange={e => {
setUsername(e.target.value)
}}
/>
<input
type="text"
placeholder="password"
onChange={e => {
setPassword(e.target.value)
}}
/>
<button type="submit">Submit</button>
</form>
)
}
render(<App />, document.getElementById('root'))
I assumed you are fresh in FE or React world. Propably you do not need that much-nested structure.
There is a special hook in React called useImperativeHandle. You can use it to call child's functions from parent.
You can find out more in the oficcial React documentation.
example of usage
Child Component
Needs to be wrapped into forwardRef like that:
export const ChildComponent = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
async functionName() {
await someLogic();
},
}));
Parent Component
In parent component you need to pass ref to the child.
Then you can use child's function this way:
const childRef = useRef(null)
childRef.current.functionName()
I want to create a custom hook useComponent which returns a JSX.Element that will be rendered elsewhere.
I have tried this:
import { useState} from 'react';
const useComponent = () => {
const [value, setValue] = useState('');
const c = () => {
return <>
<p>Component</p>
<input value={value} onChane={(e) => setValue(e.target.value)} />
</>
}
return {
c,
value,
}
}
export default function App() {
const {c: C} = useComponent();
return (
<div className="App">
<C />
</div>
);
}
but it does not work. Once I try typing on input, nothing happens.
How can I achieve this ?
I know it might be a bad practice to do such a thing, but the reason I want this is to be able to open a global dialog and pass the c component as children to the <Dialog /> component so I can both render c inside the dialog's body and also have access to the [value, setValue] state. So my use case would be something like:
[EDIT]
I also add the whole logic with dialog:
import { createContext, useContext, useState } from "react";
const Test = ({ value, setValue }) => {
return (
<>
<p>Component</p>
<input value={value} onChange={(e) => setValue(e.target.value)} />
</>
);
};
const useComponent = () => {
const [value, setValue] = useState("");
return {
element: <Test value={value} setValue={setValue} />,
value
};
};
const DialogCTX = createContext({});
export function DialogProvider(props) {
const [component, setComponent] = useState(null);
const ctx = {
component,
setComponent
};
return (
<DialogCTX.Provider value={ ctx }>
{props.children}
</DialogCTX.Provider>
);
}
export const useDialog = () => {
const {
component,
setComponent,
} = useContext(DialogCTX);
return {
component,
setComponent,
}
};
const Dialog = () => {
const { component } = useDialog();
return <div>
<p>Dialog</p>
{component}
</div>
}
const Setter = () => {
const {element, value} = useComponent();
const {setComponent} = useDialog();
return <div>
<p>Setter component</p>
<p>{value}</p>
<button onClick={() => setComponent(element)}>Set</button>
</div>
}
export default function App() {
return <div className="App">
<DialogProvider>
<Setter />
<Dialog />
</DialogProvider>
</div>;
}
As you said you want to return a JSX.Element but you actually returning a new component (a new function) every time your hook runs. So you could achieve your goal if you actually declare your component outside your hook and return the rendered one. Here is a working example:
import { useState } from "react";
const Test = ({ value, setValue }) => {
return (
<>
<p>Component</p>
<input value={value} onChange={(e) => setValue(e.target.value)} />
</>
);
};
const useComponent = () => {
const [value, setValue] = useState("");
return {
element: <Test value={value} setValue={setValue} />,
value
};
};
export default function App() {
const { element } = useComponent();
return <div className="App">{element}</div>;
}
Is there a way I can access the method triggerFoo in Child from the Parent? This is the code I have so far:
import React, {createRef} from 'react'
import ReactDOM from 'react-dom'
const Parent= () => {
let myRef = createRef()
return (
<>
<Test ref={(ref) => myRef = ref} />
<button onClick={() => myRef.triggerFoo()}>Click Me</button>
</>
)
}
const Child = React.forwardRef((props, ref) => {
const triggerFoo = () => console.log('Foo')
return <div>Testing</div>
})
Clicking on the button does not trigger anything currently.
One condition in my code is that it must use callback ref in the parent component, since this problem is part of a bigger problem I'm trying to solve involving a third party package.
Changed code.
const Parent= () => {
let myRef = createRef()
const triggerFoo = () => console.log('Foo')
return (
<>
<Child ref={myRef} triggerFoo={triggerFoo}>Click</Child>
</>
)
}
const Child = forwardRef((props, ref) => {
return <button onClick={props.triggerFoo} ref={ref}>{props.children}</button>
})
I'm playing with hooks, and I'm trying to do the following:
import React, { useState, useRef } from 'react';
const EditableField = () => {
const [isEditing, setEditing] = useState(false);
const inputRef = useRef();
const toggleEditing = () => {
setEditing(!isEditing);
if (isEditing) {
inputRef.current.focus();
}
};
return (
<>
{isExpanded && <input ref={inputRef} />}
<button onClick={toggleEditing}>Edit</button>
</>
);
};
This is going to fail, because current is null, since the component haven't re-rendered yet, and the input field is not yet rendered (and therefore can't be focused yet).
What is the right way to do this? I can use the usePrevious hook proposed in the React Hooks FAQ, but it seems like a painful workaround.
Is there a different way?
You can use the useEffect hook to run a function after every render when isEditing changed. In this function you can check if isEditing is true and focus the input.
Example
const { useState, useRef, useEffect } = React;
const EditableField = () => {
const [isEditing, setEditing] = useState(false);
const toggleEditing = () => {
setEditing(!isEditing);
};
const inputRef = useRef(null);
useEffect(() => {
if (isEditing) {
inputRef.current.focus();
}
}, [isEditing]);
return (
<div>
{isEditing && <input ref={inputRef} />}
<button onClick={toggleEditing}>Edit</button>
</div>
);
};
ReactDOM.render(<EditableField />, document.getElementById("root"));
<script src="https://unpkg.com/react#16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I know the accepted answer covers the requested element in the above question.
But as an additional note, if you are using functional components, make use of React.forwardRef to pass down the reference to child components. It might be
definitely useful for someone who refers to this question later on.
In a more cleaner way, you can write your child component which accept the ref as given below:
const InputField = React.forwardRef((props, ref) => {
return (
<div className={props.wrapperClassName}>
<input
type={props.type}
placeholder={props.placeholder}
className={props.className}
name={props.name}
id={props.id}
ref={ref}/>
</div>
)
})
Or Simply use this component
import { FC, useEffect, useRef } from 'react'
export const FocusedInput: FC<JSX.IntrinsicElements['input']> = (props) => {
const inputRef = useRef<null | HTMLElement>(null)
useEffect(() => {
inputRef.current!.focus()
}, [])
return <input {...props} type="text" ref={inputRef as any} />
}
Is it possible to receive ALL props passed in a child component without explicitly specifying them one by one? Eg:
const ParentComponent = () => {
return <ChildComponent
prop1={"foo"}
prop2={"bar"}
prop3={"baz"}
/>
}
const ChildComponent = (props) => {
return <input /*GIMME ALL PROPS HERE*/ />
}
Use the spread operator for this -
const ChildComponent = (props) => {
return <input {...props} />
}
This gets automatically interpreted as -
const ChildComponent = (props) => {
return <input prop1={"foo"}
prop2={"bar"}
prop3={"baz"} />
}