Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save julianfresco/d2268ce5a463e2983314be4ee6e051e8 to your computer and use it in GitHub Desktop.
Save julianfresco/d2268ce5a463e2983314be4ee6e051e8 to your computer and use it in GitHub Desktop.
Dev log: getting react-select, react-hook-form and react-query to play well with react v18.x and chakra-ui

Date: Monday, March 20, 2023

Coming back to React.js after some time away, I got stuck and very frustrated when integrating these libraries together with React v18.x: "@tanstack/react-query", "react-hook-form", "react-select", and "@chakra-ui/react"...

I provided a trimmed down excerpt of package.json only to capture the lib versions used during this experience.

And of course, as is common in the javascript/front-end dev community, these libraries which I had never used before have become highly-recommended, must-use libraries, so I gave them a whirl. Surprisingly, these libs were quite pleasant to use coming from previous React experience (expect for react-select).

Note: I opted to use "chakra-react-select" so I didn't have to manually match react-select's <Select /> with the styles of chakra-ui's <Select />.

Notes

Most of the problems I ran into were stupid, non-obvious setup or library gotchas.

  • React-Query scoping: the component containing <QueryClientProvider /> (e.g. <App />) cannot also use react-query hooks... In my initial attempts, the currently shown <MyForm /> component was fully contained inside my <App /> definition, and I was getting this React-Query error:

    "Error: No QueryClient set, use QueryClientProvider to set one"

    When googling the error, the fix was not obvious since I already had <QueryClientProvider client={queryClient}> in my <App /> A coworker informed me that "react-query / react-select logic needs to be down the component tree from the QueryClientProvider"... Ok, fair enough. 🤷

  • Getting React-Select to return a single value: react-select's standard approach wants options as type []RSOption where RSOption's def is an object with props "value" and "label". Further, the value returned when a user selects an item in the dropdown is also an object with RSOption props. Customizing the type of the returned user-selected value proved to be a challenge. However, I found a way by abstracting out react-select's <Select /> component, wrapping it as <MyDropdown /> and passing the react-hook-form value to <MyDropdown /> as a prop. Further, the value prop needed a mapping function, and the onChange prop must call react-hook-form's Controller's field.onChange() with the new value. The end result almost looks like a traditional react.js "controlled component", however naturally with react-select unusual style/quirks and limitations.

  • React-Hook-Form getValues: one limitation of react-hook-form's getValues() function is it reads the state once and will not update or trigger re-renders. This is for "performance" as per the docs... Anyway, while debugging my integration of the libs, this limitation misled me into thinking my setup was not working. Had I tried using the onSubmit handler, I would've seen that react-hook-form was indeed getting state changes from react-select... but I did not do look at onSubmit. I spent a significant amount of time digging into the integration-- why user selections in react-select were not updating the rendered choice via getValues... Reading the react-hook-form docs, I re-learned to use watch for rendered state values... Thus it was a non-issue. Sadly, I had learned about getValues vs watch months ago but forgot.

  • React-Select's documentation is not great. As a dev who likes to read a library's docs before heading to google/stackoverflow/youtube, I struggled to find react-select's documentation helpful when getting started. Details on the options, or how to achieve custom behavioirs, were lacking. I found it helpful to inspect other developer's gist's and stackoverflow answers for non-standard implementations, and work backwards with their overrides. Hunt and peck, not a great approach but it ultimately bore fruit.

  • React-Select's API: the interface for react-select is very custom to that library. I don't like the design, it has an Angular.js v1.x feel rather than a React.js feel. I butted heads with this library many times--too many to doucment. However, I'll leave this one: Since I am using async data to populate the dropdown via React-Query, you would think that react-select's AsyncSelect component would be the right candidate, right? ...well, you'd be wrong. It turns out, after hours trying to figure out why AsyncSelect and React-Query were not playing well, I came across this comment

    For what its worth, i was struggling with this myself and found the best option is actually not use the Async feature of react select and use reactQuery to inject the options to the normal select

    So conclusing: don't use AsyncSelect for dropdown options loaded via React-Query useQuery hooks.

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import { ChakraProvider, Container } from "@chakra-ui/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import MyForm from "./MyForm";
const App = () => {
const queryClient = new QueryClient();
return (
<ChakraProvider>
<QueryClientProvider client={queryClient}>
<Container>
<MyForm />
</Container>
</QueryClientProvider>
</ChakraProvider>
);
};
const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);
root.render(<App />);
import * as React from "react";
import { Select } from "chakra-react-select";
import { useFormContext, Controller } from "react-hook-form";
const MyDropdown = ({
inputName,
inputLabel,
options,
defaultValue,
currentValue
}) => {
const { control } = useFormContext();
return (
<Controller
name={inputName}
control={control}
render={({ field }) => (
<Select
{...field}
options={options}
defaultValue={defaultValue}
value={options.find((o) => o.value === currentValue)}
onChange={({ value }) => {
field.onChange(value);
}}
noOptionsMessage={() => `No ${inputLabel} options found.`}
/>
)}
/>
);
};
export default MyDropdown;
import * as React from "react";
import { useForm, FormProvider } from "react-hook-form";
import { Select, Button } from "@chakra-ui/react";
import { useGetOptions } from "./queries";
import MyDropdown from "./MyDropdown";
const MyForm = () => {
const inputName = "iceCreamType";
const methods = useForm({
defaultValues: {
[inputName]: "vanilla"
}
});
const {
handleSubmit,
formState: { isDirty },
watch
} = methods;
const iceCreamType_value = watch(inputName);
const onSubmit = (data) => {
console.log(
`onSubmit called receiving data = \n`,
JSON.stringify(data, null, 4)
);
};
const { isLoading, error, data } = useGetOptions();
const rsOptions = data
? data.map((o) => ({ value: o.id, label: o.name })) // Transform dropdown options for React-Select API
: [];
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<br />
<label>Ice Cream Preference</label>
{isLoading && <Select placeholder="Loading Ice Cream options..." />}
{error && (
<Select placeholder="Error! Failed to load Ice Cream options." />
)}
{data && (
<MyDropdown
inputName={inputName}
inputLabel="Ice Cream Type"
options={rsOptions}
defaultValue={"vanilla"}
currentValue={iceCreamType_value}
/>
)}
<br />
<code>
<pre>{`iceCreamType_value = ${JSON.stringify(
iceCreamType_value,
null,
4
)}`}</pre>
</code>
<Button isDisabled={!isDirty} type="submit" colorScheme="teal">
Submit
</Button>
</form>
</FormProvider>
);
};
export default MyForm;
{
"dependencies": {
"@chakra-ui/react": "^2.3.1",
"@emotion/react": "^11.10.0",
"@emotion/styled": "^11.10.0",
"@tanstack/react-query": "^4.10.3",
"@types/node": "^12.20.55",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"chakra-react-select": "^4.6.0",
"framer-motion": "^6.5.1",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.34.2",
"react-select": "^5.7.0",
"typescript": "^4.8.2",
},
"devDependencies": {
"@tanstack/react-query-devtools": "^4.24.6"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment