Maintaining multiple user inputs to antd Select in a single useState variable - reactjs

I want to store the information of multiple inputs entered into antd Select components in a single state variable but am having trouble getting the below to work.
This example is solved here for a form but the same solution doesn't seem to work for antd Select component. There are two inputs: a first name and a last name that I want to remember. The below code doesn't work because e doesn't have an attribute called name is what the console tells me. I also tried e.target.name and e.target.value but I get an error that e doesn't have an attribute called a target either. What is the right way to do this?
import React, { useState } from 'react';
import { Select } from 'antd';
const App = () =>{
const [varState, setVarState] = useState({firstName:'Jack', lastName:'Smith'});
const firstNameOptions = [ {label:'Jack', value:'Jack'}, {label:'Jill',value:'Jill'}, {label:'Bill',value:'Bill'} ];
const lastNameOptions = [ {label:'Smith', value:'Smith'}, {label:'Potter',value:'Potter'}, {label:'Bach',value:'Bach'} ];
const changeState = (e) => {
setVarState( prevState => ({ ...prevState, [e.name]: e.value}));
console.log(varState)
};
return ( <>
<div>
<Select name={'firstName'} defaultValue={'Pick One'} options={firstNameOptions} onChange={changeState} />
<Select name={'lastName'} defaultValue={'Pick One'} options={lastNameOptions} onChange={changeState} />
</div>
</>
);
}
export default App;
At the heart of it, it seems that I don't know how to name the Select components in such a way that their names can be passed on to the onChange handler.
More generally, given a component like antd Select, how can I figure out what the right "name field" is for this component so that it's value can be passed on to an onChange handler? For instance, what in the documentation for select gives this information?

The Select component seems to be sending the value instead of the events object. So, You can just make a closure and pass the name of the select. Also, for consoling you can make use of a useEffect which only consoles when the state has been updated. Otherwise, you could see previous state as state updates are asynchronous. Below is a working solution.
import React, { useEffect, useState } from "react";
import { Select } from "antd";
const App = () => {
const [varState, setVarState] = useState({
firstName: "Jack",
lastName: "Smith"
});
const firstNameOptions = [
{ label: "Jack", value: "Jack" },
{ label: "Jill", value: "Jill" },
{ label: "Bill", value: "Bill" }
];
const lastNameOptions = [
{ label: "Smith", value: "Smith" },
{ label: "Potter", value: "Potter" },
{ label: "Bach", value: "Bach" }
];
// for consoling when the state updates
useEffect(() => {
console.log(varState);
}, [varState.firstName, varState.lastName]);
const changeState = (value, identifier) => {
// console.log(value, identifier);
setVarState((prevState) => ({ ...prevState, [identifier]: value }));
};
return (
<>
<div>
<Select
name={"firstName"}
defaultValue={"Pick One"}
options={firstNameOptions}
onChange={(val) => changeState(val, "firstName")}
/>
<Select
name={"lastName"}
defaultValue={"Pick One"}
options={lastNameOptions}
onChange={(val) => changeState(val, "lastName")}
/>
</div>
</>
);
};
export default App;

yes, Actually antd doesn't have attribute name for input fields. antdesign directly gives the selected value, we need to do some tweeks to achieve this.
Here is the solution:
import React, { useState } from 'react';
import { Select } from 'antd';
const firstNameOptions = [ {label:'Jack', value:'Jack'}, {label:'Jill',value:'Jill'}, {label:'Bill',value:'Bill'} ];
const lastNameOptions = [ {label:'Smith', value:'Smith'}, {label:'Potter',value:'Potter'}, {label:'Bach',value:'Bach'} ];
const App = () =>{
const [varState, setVarState] = useState(null);
const changeState = (fieldName) => (value) => {
setVarState( prevState => ({ ...prevState, [fieldName]: value}));
console.log(varState)
};
return ( <>
<div>
<Select defaultValue={'Pick One'} options={firstNameOptions} onChange={changeState('firstName')} />
<Select defaultValue={'Pick One'} options={lastNameOptions} onChange={changeState('lastName')} />
</div>
</>
);
}
export default App;
I hope this helps 😊

Related

Change a checkbox checked state from a useEffect hook in React?

The value of context.number is 1. I want to set the input checked value to true based off the context.number. So if context.number is 1, the checkbox is checked for Module 1, showing that Module 1 is completed. I can only change the input value from the onChange event though, so i assume i need a useEffect hook so it does it automatically when context.number changes?
import React, { useState, useContext, useEffect } from "react";
import { Link } from "react-router-dom";
import { AppContext } from "../context/AppContext";
export default function Menu() {
const context = useContext(AppContext);
//Determine the modules
const modules = [
{
title: `Introduction`,
subtitle: "Lesson 1",
},
{
title: `Overview`,
subtitle: "Lesson 2",
}
];
//Create a checked state for each module
const [checkedState, setCheckedState] = useState(new Array(modules.length).fill(false));
//Change checked value method
const handleOnChange = (position) => {
//map through checked states array, if position === mapped item index, flip the value
const updatedCheckedState = checkedState.map((item, index) => (index === position ?
!item : item));
//set that items state to the new value in the array
setCheckedState(updatedCheckedState);
};
return (
<>
<div>
{modules.map((module, index) => (
<div className={styles.menuCard}>
<Link key={index} to={`/Module/${index + 1}`}>
<h2>{module.title}</h2>
<p>{module.subtitle}</p>
</Link>
<input
id={`check${index}`}
type="checkbox"
onChange={() => handleOnChange(index)}
checked={checkedState[index]}
/>
</div>
))}
</div>
</>
);
}
I put this is my context file and was able to get it to work. This is a poorly worded question I realize but found my answer none the less. useEffect wasn't the problem, it was that each checkbox was only saving local state so if i rendered another page the checkboxes went back to being unchecked.
import React, { createContext, useState } from "react";
const AppContext = createContext();
function AppProvider(props) {
//Determine the modules
const modules = [
{
title: `Introduction`,
subtitle: "Lesson 1",
},
{
title: `Overview`,
subtitle: "Lesson 2",
}
];
//set module number state
const [number, setNumber] = useState();
//Create a checked state for each module
const [checkedState, setCheckedState] = useState(new Array(modules.length).fill(false));
//change module number method
function completionHandler(value) {
setNumber(value);
}
//Change checked value method
function handleChange(position) {
const updatedCheckedState = checkedState.map((item, index) => (index == position ? !item : item));
setCheckedState(updatedCheckedState);
}
//export method and state value
const value = {
number: number,
modules: modules,
checkedState: checkedState,
handleChange: handleChange,
completionHandler: completionHandler,
};
return <AppContext.Provider value={value}>{props.children}</AppContext.Provider>;
}
export { AppContext, AppProvider };

Connecting Slate to Formik - is it useField?

Here's the React component I'm creating to host Slate:
import React, { useMemo, useState } from "react";
import { Field, useField, ErrorMessage } from "formik";
import { Slate, Editable, withReact } from "slate-react";
const FormRichText = ({ label, ...props }) => {
const [field, meta] = useField(props);
const editor = useMemo(() => withReact(createEditor()), []);
const [editorContent, setEditorContent] = useState(field.value);
field.value = editorContent;
return (
<Slate
{...field}
{...props}
name="transcript"
editor={editor}
onChange={(v) => setEditorContent(v)}
>
<Editable />
</Slate>
);
};
export default FormRichText;
The issue I'm having is when I try to connect it into Formik, whatever I edit will not pass to the Formik values in handleSubmit.
<FormRichText
name="transcript"
id="transcript"
/>
I don't understand Formik which is why I'm having the issue. I believe I need to surface the value of Slate (which I've stored in a state variable) but I'm struggling to work my way through Formik to know how to do that. I assumed that if I use the userField prop I would be able to set field.value and that would get through to Formik.
This worked:
const FormRichText = ({ label, ...props }) => {
const [field, meta, helpers] = useField(props.name);
const editor = useMemo(() => withReact(createEditor()), []);
const { value } = meta;
const { setValue } = helpers;
useEffect(() => {
setValue([
{
type: "paragraph",
children: [{ text: "Type something ..." }],
},
]);
}, []);
return (
<Slate name="transcript"
value={value}
editor={editor}
onChange={(v) => setValue(v)}>
<Editable />
</Slate>
</div>
</div>
);
};
Hopefully this helps someone else.
Saying that, I still have to render content through Slate so I may be back on this thread!

Testing React Context update

I have been struggling to test a React Context update for a while. I can predefine the value of the Context.Provider however, the solution is not ideal because an update to the context which is supposed to happen within component utilising the context is not actually happening.
When I test this manually the text 'Account name: abc' changes to 'Account name: New account name' but not in the test. The context value remains the same.
The reason I predefine the value is because the component relies on a fetch response from a parent and I am unit testing <ImportAccounts /> only.
In a test I am rendering a component with predefined value
test('accountName value is updated on click', () => {
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
expect(getByText('Account name: abc')).toBeInTheDocument();
const input = getByLabelText('Account name');
fireEvent.change(input, { target: { value: 'New account name' } });
fireEvent.click(getByRole('button', { name: 'Update' }));
expect(getByText('Account name: New account name')).toBeInTheDocument();
});
Here's my context
import React, { createContext, useContext, useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface StateVariable<T> {
value: T;
setValue: Dispatch<SetStateAction<T>>;
}
export interface ImportAccountsState {
accountName: StateVariable<string>;
}
export const ImportAccountsContext = createContext<ImportAccountsState>(
{} as ImportAccountsState,
);
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};
Import Accounts is as simple as
export const ImportAccounts = () => {
const { accountName } = useImportAccountsContext();
const [newAccountName, setNewAccountName] = useState(accountName.value);
const handleAccountNameChange = () => {
accountName.setValue(newAccountName);
};
return (
<>
<h1>Account name: {accountName.value}</h1>
<label htmlFor="accountName">Account name</label>
<input
value={newAccountName}
onChange={e => setNewAccountName(e.target.value)}
id="accountName"
/>
<button
type="button"
onClick={handleAccountNameChange}>
Update
</button>
</>
);
}
How can I test that accountName has actually updated?
If we don't need the default value for the ImportAccounts provider then we make the test pass easily. ImportAccountsProvider manages the state of the accountName within itself. In that provider, we are passing the accountName state of type ImportAccountsState to all our children through the context provider.
Now coming to your problem,
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
Here, the value: 'abc' is not a state value, it's simply a string constant 'abc' which will never be going to change. This is something that we should note. We must pass the state value to the context provider if we want to share the value with the children which is not going to be constant in the entire react lifecycle.
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
test('should update the context with existing provider', () => {
render(
<ImportAccountsProvider>
<ImportAccounts />
</ImportAccountsProvider>
);
// some constant hoisted to make it clean code
const accountInput = screen.getByRole('textbox', { name: /account name/i });
const accountInputValue = 'subrato patnaik';
expect(accountInput).toHaveAttribute('value', '');
// do some changes
userEvent.type(accountInput, accountInputValue);
//validate "changes"
expect(screen.getByDisplayValue(accountInputValue)).toBeTruthy();
expect(accountInput).toHaveAttribute('value', accountInputValue);
// update context
userEvent.click(screen.getByRole('button', { name: /update/i }));
// validate update
expect(screen.getByRole('heading')).toHaveTextContent(/subrato/i);
screen.debug();
});
Inside the ImportAccountsProvider we can do the fetch call and set the accountName state to the response of the fetch call.
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
useEffect(() => {
// do the fetch call here and update the accountName state accordingly
});
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};

React Multiple Checkboxes are not visible as selected after change

I have a list of checkboxes.
Checkbox is not visible as selected even after the value has been changed.
Below is my code: -
import React, { useState } from "react";
import { render } from "react-dom";
const CheckboxComponent = () => {
const [checkedList, setCheckedList] = useState([
{ id: 1, label: "First", isCheck: false },
{ id: 2, label: "Second", isCheck: true }
]);
const handleCheck = (e, index) => {
checkedList[index]["isCheck"] = e.target.checked;
setCheckedList(checkedList);
console.log(checkedList);
};
return (
<div className="container">
{checkedList.map((c, index) => (
<div>
<input
id={c.id}
type="checkbox"
checked={c.isCheck}
onChange={e => handleCheck(e, index)}
/>
<label htmlFor={c.id}>{c.label}</label>
</div>
))}
</div>
);
};
render(<CheckboxComponent />, document.getElementById("root"));
I was working fine for a simple checkbox outside the loop.
I am not sure where is the problem.
Here is the link - https://codesandbox.io/s/react-multiple-checkboxes-sczhy?file=/src/index.js:0-848
Cause you pass an array to the state, so if you want your react component re-render, you must let the react know that your state change. On your handleCheck, you only change property of an value in that array so the reference is not changed.
The handleCheck function should be look like this
const handleCheck = (e, index) => {
const newCheckList = [...checkedList];
newCheckList[index]["isCheck"] = e.target.checked;
setCheckedList(newCheckList);
};
Do this instead:
const handleCheck = (e, index) => {
setCheckedList(prevState => {
const nextState = prevState.slice()
nextState[index]["isCheck"] = e.target.checked;
return nextState
});
};
Since checkedList is an array, (considered as object and handled as such), changing a property won't change the array itself. So React can know that something changed.

How to Jest/Unit Test a Component Reliant on TableRefs

I have a table which given the downloadRequested it will use the tableRef provided and return the sortedData within the table. When trying to add unit tests to make sure that the <CSVLink> is rendered on downloadRequested = true I keep hitting the below error:
TypeError: Cannot read property 'getResolvedState' of null
Am I supposed to mock the ref? or do I need to somehow provide it? How do I get around this issue?
Code:
import React, { useRef } from "react";
import { CSVLink } from "react-csv";
function MyTable(props) {
const tableRef = useRef(null);
const getColumns = () => {
return [{ Header: "Name", accessor: "name" }, { Header: "Id", accessor: "id" }];
};
const getCsvData = () => {
const keys = ["name", "id"];
return tableRef.current.getResolvedState().sortedData.map(row => getCsvDataFromTable(keys, row));
};
return (
<>
{props.downloadRequested && (
<CSVLink data={getCsvData()} target="_blank" filename={`myTable.csv`} data-testid="csvLink">
<div
data-testid="csvLinkDiv"
ref={e => {
if (e) {
e.click();
}
}}
/>
</CSVLink>
)}
<Table columns={getColumns(props)} filterable forwardedRef={tableRef} {...props} />
</>
);
}
export default MyTable;
Test Suite:
import React from "react";
import { render } from "#testing-library/react";
import MyTable from "./MyTable";
describe("MyTable", () => {
const sampleData = [{ id: "123", name: "John Doe" }, { id: "456", name: "Doe John" }];
it("Should render MyTable with CSV Link correctly", () => {
const { queryByTestId } = render(<MyTable data={sampleData} downloadRequested={true} />);
expect(queryByTestId("csvLink")).toBeTruthy();
});
});
Well, I figured this out. My problem stems from the fact that the CSV-Link needs to have data at run time, and the way this is set up it doesnt get that till later. Anyways, what was happening is that getCsvData() was being called before the table was initialized, hence the ref was always null. One work around I used was to render it with downloadRequested = false first, then rerender it (now a table exists) with the value true.

Resources