I am trying to bind a handler to an array position that prints the state of the object. The function is being called but the state is always in the initial state.
import React from "react";
export default function MyForm() {
const [state, setState] = React.useState({
buttons: [],
object: null
});
const stateRef = React.useRef(null);
const onExecButtonHandler = (index) => {
console.log("[MyForm][execButton]: " + index);
alert(JSON.stringify(state.object));
alert(JSON.stringify(stateRef.current.object));
};
const onChangeHandler = (event)=>{
let obj = state.object
obj['description'] = event.target.value
console.log(obj)
setState((state) => ({ ...state, object: obj }));
stateRef.current = state;
}
const generateButtons = () => {
let buttons = [];
//buttons.push([name,(event)=>onExecButtonHandler(event,i),obj[i].icon,obj[i].jsFunction])
buttons.push([
"Show Hi",
onExecButtonHandler.bind(this, 1),
"icon",
"custom function"
]);
return buttons;
};
React.useEffect(() => {
console.log("[MyForm][useEffect]");
let buttons = generateButtons();
setState({ buttons: buttons, object: { description: "hi" } });
stateRef.current = state;
}, []);
return (
<>
{state.object && (
<form>
<input text="text" onChange={onChangeHandler} value={state.object.description} />
<input
type="button"
value="Click me"
onClick={() => {
state.buttons[0][1]();
}}
/>
</form>
)}
</>
);
}
You can test here: https://codesandbox.io/s/charming-robinson-g1yuo?fontsize=14&hidenavigation=1&theme=dark
I was able to access the latest state of the object using the "useRef" hook. I'd like to access using the state though. Is there some way to do so?
Related
I have two children Components, when I onChange in first children, then the second children re render, I don't want to the second children re render. Online code example:
https://codesandbox.io/s/ji-ben-antd-4-24-0-forked-efg56l?file=/demo.tsx
const ChildA = (props: {
name: string;
changeValue: (key: string, value: any) => void;
}) => {
const { name, changeValue } = props;
return (
<Input
value={name}
onChange={(e) => {
changeValue("name", e.target.value);
}}
/>
);
};
const ChildB = (props: {
age: number;
changeValue: (key: string, value: any) => void;
}) => {
const { age, changeValue } = props;
console.log("==when I change name====, this component re-render");
return (
<InputNumber
value={age}
onChange={(e) => {
changeValue("age", e);
}}
/>
);
};
const App: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [saveValue, setSaveValue] = useState({
name: "wang",
age: 18
});
const showModal = () => {
setIsModalOpen(true);
};
const handleOk = () => {
// send value
console.log("====saveValue==", saveValue);
setIsModalOpen(false);
};
const handleCancel = () => {
setIsModalOpen(false);
};
const changeValue = (key: string, value: any) => {
const newValue = JSON.parse(JSON.stringify(saveValue));
newValue[key] = value;
setSaveValue(newValue);
};
return (
<>
<Button type="primary" onClick={showModal}>
Open Modal
</Button>
<Modal
title="Basic Modal"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
>
<ChildA name={saveValue?.name} changeValue={changeValue} />
<ChildB age={saveValue?.age} changeValue={changeValue} />
</Modal>
</>
);
};
When I change the name ,I don`t want to Child B re-render
The actual situation is that there are many sub-components in a Modal. When you click OK, the value of the sub-component is obtained, saved and sent to the server. If you have good design component ideas, please share
I don't want to the second children re render.
Wrap ChildB with React.memo for a basic memoization.
const ChildB = memo(...);
Wrap the changeValue function with React.useCallback to persist the instance.
const changeValue = useCallback(...);
Slightly modify the changeValue function so it does not use the saveValue as a dependency.
setSaveValue((prev) => {
const newValue = JSON.parse(JSON.stringify(prev));
newValue[key] = value;
return newValue;
});
Codesandbox demo
I have an ant design tab group with an ant design form in each tab. I would like that upon switching tabs, the user is prompted to submit their changes. If the user selects yes, then the data should be submitted. We switch tabs only if the response comes back as a success, or the user opted not to save.
The forms are all child components, which means the parent needs to somehow indicate that the tab is switching, and tell the child to submit their form.
I can achieve this with the useImperativeHandle hook and forwardRef but I'm wondering if there is some non-imperative way to achieve this?
Here is a stripped down example, not checking if the form is dirty, and just using the native confirm popup. There is also an async function to simulate submitting the form, which will randomly succeed or error.
Stackblitz: https://stackblitz.com/edit/react-ts-zw2cgi?file=my-form.tsx
The forms:
export type FormRef = { submit: (data?: Data) => Promise<boolean> };
export type Data = { someField: string };
const MyForm = (props: {}, ref: Ref<FormRef>) => {
const [form] = useForm<Data>();
useImperativeHandle(ref, () => ({ submit }));
async function submit(data?: Data): Promise<boolean> {
if (!data) data = form.getFieldsValue();
const res = await submitData(data);
return res;
}
return (
<Form form={form} onFinish={submit}>
<Form.Item name="someField">
<Input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
);
};
export default forwardRef(MyForm);
The parent component with the tabs:
const App: FC = () => {
const [activeKey, setActiveKey] = useState('tabOne');
const formOneRef = useRef<FormRef>();
const formTwoRef = useRef<FormRef>();
async function onChange(key: string) {
if (confirm('Save Changes?')) {
if (activeKey === 'tabOne' && (await formOneRef.current.submit()))
setActiveKey(key);
if (activeKey === 'tabTwo' && (await formTwoRef.current.submit()))
setActiveKey(key);
} else setActiveKey(key);
}
const tabs = [
{
label: 'Tab One',
key: 'tabOne',
children: <MyForm ref={formOneRef} />,
},
{
label: 'Tab Two',
key: 'tabTwo',
children: <MyForm ref={formTwoRef} />,
},
];
return <Tabs items={tabs} onChange={onChange} activeKey={activeKey} />;
};
The submit function
export default async function submitData(data: Data) {
console.log('Submitting...', data);
const res = await new Promise<boolean>((resolve) =>
setTimeout(
() => (Math.random() < 0.5 ? resolve(true) : resolve(false)),
1000
)
);
if (res) {
console.log('Success!', data);
return true;
}
if (!res) {
console.error('Fake Error', data);
return false;
}
}
Ant Design Tabs: https://ant.design/components/tabs/
Ant Design Form: https://ant.design/components/form/
I ended up making a state variable to store the submit function, and setting it in the child with useEffect.
A few caveats:
Had to set destroyInactiveTabPane to ensure forms were unmounted and remounted when navigating, so useEffect was called.
Had to wrap the form's submit function in useCallback as it was now a dependency of useEffect.
Had to make sure when calling setSubmitForm I had passed a function that returns the function, else the dispatch just calls submit immediately.
Stackblitz: https://stackblitz.com/edit/react-ts-n8y3kh?file=App.tsx
export type Data = { someField: string };
type Props = {
setSubmitForm: Dispatch<SetStateAction<() => Promise<boolean>>>;
};
const MyForm: FC<Props> = ({ setSubmitForm }) => {
const [form] = useForm<Data>();
const submit = useCallback(
async (data?: Data): Promise<boolean> => {
if (!data) data = form.getFieldsValue();
const res = await submitData(data);
return res;
},
[form]
);
useEffect(() => {
setSubmitForm(() => submit);
}, [setSubmitForm, submit]);
return (
<Form form={form} onFinish={submit}>
<Form.Item name="someField">
<Input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
);
};
const App: FC = () => {
const [activeKey, setActiveKey] = useState('tabOne');
const [submitForm, setSubmitForm] = useState<() => Promise<boolean>>(
async () => true
);
async function onChange(key: string) {
if (confirm('Save Changes?')) {
if (await submitForm()) setActiveKey(key);
} else setActiveKey(key);
}
const tabs = [
{
label: 'Tab One',
key: 'tabOne',
children: <MyForm setSubmitForm={setSubmitForm} />,
},
{
label: 'Tab Two',
key: 'tabTwo',
children: <MyForm setSubmitForm={setSubmitForm} />,
},
];
return (
<Tabs
items={tabs}
onChange={onChange}
activeKey={activeKey}
destroyInactiveTabPane
/>
);
};
I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows.
So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).
So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined.
Any suggestions on how to handle this?
This is what I got so far.
The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my template for the resp. controlled form components I want to use:
export const ControlledGenericForm = ({ formData, onChange }) => {
return (
<form>
{Object.keys(formData).map((formElementKey) => (
<input
key={formElementKey}
value={formData[formElementKey]}
type="text"
id={formElementKey}
onInput={(event) => onChange(event.target.id, event.target.value)}
/>
))}
</form>
);
};
That's my controlled Onboarding component, I want to use:
import React from "react";
export const ControlledOnboardingFlow = ({
children,
currentIndex,
onPrevious,
onNext,
onFinish,
}) => {
const goToNext = (stepData) => {
onNext(stepData);
};
const goToPrevious = (stepData) => {
onPrevious(stepData);
};
const goToFinish = (stepData) => {
onFinish(stepData);
};
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
// HOW TO DECIDE?!
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
if (currentChild && onPrevious)
return React.cloneElement(currentChild, { goToPrevious });
return currentChild;
};
And that's the actual use of this two components within my App:
import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";
function App() {
const [onboardingData, setOnboardingData] = useState({
name: "Juh",
age: 22,
hair: "green",
street: "Main Street",
streetNo: 42,
city: "NYC",
});
const [currentIndex, setCurrentIndex] = useState(0);
const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
onboardingData
);
const formDataPartTwo = (({ street, streetNo, city }) => ({
street,
streetNo,
city,
}))(onboardingData);
const onNext = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex + 1);
};
const onPrevious = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex - 1);
};
const onFinish = () => {
console.log("Finished");
console.log(onboardingData);
};
const handleFormUpdate = (id, value) => {
setOnboardingData({ ...onboardingData, [id]: value });
};
const StepOne = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 1</h1>
<ControlledGenericForm
formData={formDataPartOne}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)} >
Prev
</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepTwo = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 2</h1>
<ControlledGenericForm
formData={formDataPartTwo}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)}>Prev</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepThree = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 3</h1>
<h3>
Congrats {onboardingData.name} for being from, {onboardingData.city}
</h3>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
return (
<ControlledOnboardingFlow
currentIndex={currentIndex}
onPrevious={onPrevious}
onNext={onNext}
onFinish={onFinish}
>
<StepOne />
<StepTwo />
{onboardingData.city === "NYC" && <StepThree />}
</ControlledOnboardingFlow>
);
}
export default App;
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.
It looks like you want to pass both functions into the child, which can be done like:
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
if (currentChild) {
return React.cloneElement(currentChild, { goToNext, goToPrevious });
}
return currentChild;
If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.
Im having trouble with this little test component what am I missing here? This is just a simple little teaser question, seems like the mapping is where is breaking. I don't know if Im passing the value down incorrectly or what
import React from "react";
export default function App() {
const [state, setState] = React.useState({
input: "",
items: [],
error: false
});
const { input, items, error } = state;
const changeState = (state) => {
return setState(state);
};
if (input.length > 10) {
React.useEffect(() => {
changeState({ error: false });
});
}
const handleUpdateInput = (e) => {
if (e.key === "Enter") {
changeState({ items: [e.target.value, ...items] });
changeState({ input: "" });
}
};
const removeItem = (item) => {
changeState({ items: items.filter((i) => i !== item) });
};
return (
<div>
<input
onChange={(e) => {
changeState({ input: e.target.value });
}}
value={input}
onKeyPress={handleUpdateInput}
/>
{items.map((item, index) => {
return (
<div key={index}>
{item} <span onClick={removeItem(item)}>X</span>
</div>
);
})}
{error && <div>Input is too long!</div>}
</div>
);
}
When you set state using hooks, the value passed in will be the exact new state - it won't be merged with the old state if it happens to be an object, as with class components.
So, for example, you need to change
changeState({ error: false });
to
changeState({ ...state, error: false });
and do the same for everywhere else you're setting state.
But a better approach (that I'd recommend, and that React recommends) would be to use separate state variables from the beginning:
const [items, setItems] = useState([]);
Set it by changing
changeState({ items: [e.target.value, ...items] });
to
setItems([e.target.value, ...items]);
and follow that same pattern for input and error too.
I have a question, if I can use useState generic in React Hooks, just like I can do this in React Components while managing multiple states?
state = {
input1: "",
input2: "",
input3: ""
// .. more states
};
handleChange = (event) => {
const { name, value } = event.target;
this.setState({
[name]: value,
});
};
Yes, with hooks you can manage complex state (without 3rd party library) in three ways, where the main reasoning is managing state ids and their corresponding elements.
Manage a single object with multiple states (notice that an array is an object).
Use useReducer if (1) is too complex.
Use multiple useState for every key-value pair (consider the readability and maintenance of it).
Check out this:
// Ids-values pairs.
const complexStateInitial = {
input1: "",
input2: "",
input3: ""
// .. more states
};
function reducer(state, action) {
return { ...state, [action.type]: action.value };
}
export default function App() {
const [fromUseState, setState] = useState(complexStateInitial);
// handle generic state from useState
const onChangeUseState = (e) => {
const { name, value } = e.target;
setState((prevState) => ({ ...prevState, [name]: value }));
};
const [fromReducer, dispatch] = useReducer(reducer, complexStateInitial);
// handle generic state from useReducer
const onChangeUseReducer = (e) => {
const { name, value } = e.target;
dispatch({ type: name, value });
};
return (
<>
<h3>useState</h3>
<div>
{Object.entries(fromUseState).map(([key, value]) => (
<input
key={key}
name={key}
value={value}
onChange={onChangeUseState}
/>
))}
<pre>{JSON.stringify(fromUseState, null, 2)}</pre>
</div>
<h3>useReducer</h3>
<div>
{Object.entries(fromReducer).map(([key, value]) => (
<input
name={key}
key={key}
value={value}
onChange={onChangeUseReducer}
/>
))}
<pre>{JSON.stringify(fromReducer, null, 2)}</pre>
</div>
</>
);
}
Notes
Unlike the setState method found in class components, useState does not automatically merge update objects. You can replicate this behavior by combining the function updater form with object spread syntax:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Refer to React Docs.
The correct way to do what you're trying to do is to create your own hook that uses useState internally.
Here is an example:
// This is your generic reusable hook.
const useHandleChange = (initial) => {
const [value, setValue] = React.useState(initial);
const handleChange = React.useCallback(
(event) => setValue(event.target.value), // This is the meaty part.
[]
);
return [value, handleChange];
}
const App = () => {
// Here we use the hook 3 times to show it's reusable.
const [value1, handle1] = useHandleChange('one');
const [value2, handle2] = useHandleChange('two');
const [value3, handle3] = useHandleChange('three');
return <div>
<div>
<input onChange={handle1} value={value1} />
<input onChange={handle2} value={value2} />
<input onChange={handle3} value={value3} />
</div>
<h2>States:</h2>
<ul>
<li>{value1}</li>
<li>{value2}</li>
<li>{value3}</li>
</ul>
</div>
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Note the use of React.useCallback to stop your hook from returning a new handler function on every render. (We don't need to specify setValue as a dependency because React guarantees that it will never change)
I didn't actually test this, but it should work.
See https://reactjs.org/docs/hooks-reference.html#usestate for more info.
import React, {useState} from 'react';
const MyComponent = () => {
const [name, setName] = useState('Default value for name');
return (<div><button onClick={()=>setName('John Doe')}}>Set Name</button></div>);
};
export default MyComponent;