Why is useImperitaveHandle hook used in React? - reactjs

I am not able to find a use Case for useImperativeHandle Hook. Trying to Google and understand I came across a code sandbox showing an example of why the useImperitaveHandle Hook would be used. Here is the link to the codesandbox
https://codesandbox.io/s/useimperativehandle-example-forked-illie?file=/src/App.js
I modified the code to get it working without the useImperitaveHandle in the codesandbox link below. Can someone explain why the hook would be used as I believe that code can be written without it to provide the exact same functionality.
https://codesandbox.io/s/useimperativehandle-example-forked-2cjdc?file=/src/App.js

I found an example where this would be used. According to my understanding it will be mainly needed if you need to write some custom functionality for a library and will need some of the library's in built features.
In the example I will provide, I will write a custom CellEditor for the library ag-grid(Table library) because I want to select the value of the cell in the table using Material UI's autocomplete. Below is the code
import React, { useState, forwardRef, useImperativeHandle } from "react";
import MuiTextField from "#material-ui/core/TextField";
import MuiAutocomplete from "#material-ui/lab/Autocomplete";
const AutocompleteEditor = forwardRef(
({ fieldToSave, fieldToShow, textFieldProps, options, ...props }, ref) => {
const [value, setValue] = useState("");
useImperativeHandle(ref, () => {
return {
getValue: () => {
return value;
},
afterGuiAttached: () => {
setValue(props.value);
},
};
});
const tranformValue = (value, fieldtosave) =>
Array.isArray(value)
? value.map((v) => v[fieldtosave] || v)
: value[fieldtosave];
function onChangeHandler(e, value) {
setValue(value ? tranformValue(value, fieldToSave) : null);
}
return (
<MuiAutocomplete
style={{ padding: "0 10px" }}
options={options}
getOptionLabel={(item) => {
return typeof item === "string" || typeof item === "number"
? props.options.find((i) => i[fieldToSave] === item)[fieldToShow]
: item[fieldToShow];
}}
getOptionSelected={(item, current) => {
return item[fieldToSave] === current;
}}
value={value}
onChange={onChangeHandler}
disableClearable
renderInput={(params) => (
<MuiTextField
{...params}
{...textFieldProps}
style={{ padding: "5px 0" }}
placeholder={"Select " + props.column.colId}
/>
)}
/>
);
}
);
export default AutocompleteEditor;
IGNORE THE AUTOCOMPLETE PART IF ITS CONFUSING, ITS NOT IMPORTANT FOR UNDERSTANDING useImperativeHandler
So To explain what the code does. The value of the above component is set by Autocomplete by the user, but ag-grid table also needs the value as it needs to update the value in corresponding cell.
It uses a ref internally that it passes to the customCellEditor. The useImperativeHook then tells ag-grid to use the value from the state of the component whenever getValue is called (it is called when the cell needs to display the value)

Related

Material UI Autocomplete not updating input value (React)

I made an Autocomplete component in React using Material UI's Autocomplete component. Here's the code
import { useState } from "react";
import { Autocomplete as MuiAutcomplete } from "#mui/material";
import {useFormContext} from "react-hook-form";
interface props {
name: string,
options?: string[],
getOptions?: (value: string) => {
label: string,
id: number
}[] | string[],
freeSolo?: boolean
};
const Autocomplete = ({name, options=[], getOptions, freeSolo=false}: props) => {
const [autocompleteValues, setAutocompleteValues] = useState<any[]>(options);
const {setValue, getValues} = useFormContext();
return (
<MuiAutcomplete
options={autocompleteValues}
renderInput={({ InputProps, inputProps }) => (
<div ref={InputProps.ref}>
<input
type="text"
{...inputProps}
className="bg-transparent outline-none p-1"
/>
</div>
)}
value={getValues(name)}
onChange={(e, v) => {
setValue(name, v);
}}
getOptionLabel={(option) => option.label || option}
freeSolo={freeSolo}
/>
)
}
export default Autocomplete;
The options display just fine when I type but when actually selecting an option the input field doesn't actually get updated. It instead shows this error:
`MUI: The value provided to Autocomplete is invalid.None of the options match with `""`.You can use the `isOptionEqualToValue` prop to customize the equality test. `
I'm not entirely sure what's going on. Here's a video showing the error in case you need clarification https://imgur.com/a/xfm1mpb (sorry for low res, Imgur's compression ruined it)
Can you please details about what options are you passing?
It will be better if you can provide a codesandbox.
It is due to the option which you are selecting is not matching the value in options
The "" value is an actual value for auto complete. If you want to make it work with an empty field you can:
// Set value to null. If you set it to 'undefined' it will give you a warning similar to the current one
value={getValues(name) || null}
Or
// Override the function which checks if the value is equal to the option.
// Basically, you handle the empty string here.
isOptionEqualToValue={(option, currentValue) => {
if (currentValue === '') return true;
return option.name === currentValue.name;
}}

Chakra UI - Checkbox Group

I am using Chakra UI with React Typescript and implementing a checkbox group
The default values are controlled by an outside state that is passed down as a prop.
The problem is that the CheckboxGroup doesn't accept the default values from outside source
The code is as follows:
import React, {FC, useCallback, useEffect, useState} from "react";
import { CheckboxGroup, Checkbox, VStack } from "#chakra-ui/react";
interface IGroupCheckbox {
values: StringOrNumber[],
labels: StringOrNumber[],
activeValues: StringOrNumber[]
onChange: (value:StringOrNumber[])=> void
}
const GroupCheckbox:FC<IGroupCheckbox> = ({
values,
labels,
activeValues,
onChange
}) => {
const [currActiveValues, setCurrActiveValues] = useState<StringOrNumber[]>();
const handleChange = useCallback((value:StringOrNumber[]) => {
if(value?.length === 0) {
alert('you must have at least one supported language');
return;
}
onChange(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(()=>{
if(activeValues) {
setCurrActiveValues(['en'])
}
},[activeValues])
return (
<CheckboxGroup
onChange={handleChange}
defaultValue={currActiveValues}
>
<VStack>
{values && labels && values.map((item:StringOrNumber, index:number)=>
{
return (
<Checkbox
key={item}
value={item}
>
{labels[index]}
</Checkbox>
)
}
)}
</VStack>
</CheckboxGroup>
)
}
export default GroupCheckbox
When I change the defaultValue parameter, instead of the state managed, to be defaultValue={['en']} it works fine, but any other input for this prop doesn't work.
I checked and triple checked that the values are correct.
Generally, passing a defaultValue prop "from an outside source" actually does work. I guess that in your code, only ['en'] works correctly because you explicitly use setCurrActiveValues(['en']) instead of setCurrActiveValues(activeValues).
The defaultValue prop will only be considered on the initial render of the component; changing the defaultValue prop afterwards will be ignored. Solution: make it a controlled component, by using the value prop instead of defaultValue.
Note that unless you pass a parameter to useState(), you will default the state variable to undefined on that initial render.
Side note: You also don't need a separate state variable currActiveValues. Instead, you can simply use activeValues directly.
const GroupCheckbox = ({ values, labels, activeValues, onChange }: IGroupCheckbox) => {
const handleChange = useCallback(
(value: StringOrNumber[]) => {
if (value?.length === 0) {
alert("you must have at least one supported language")
return
}
onChange(value)
},
[onChange]
)
return (
<CheckboxGroup onChange={handleChange} value={activeValues}>
<VStack>
{labels && values?.map((item: StringOrNumber, index: number) => (
<Checkbox key={item} value={item}>
{labels[index]}
</Checkbox>
))}
</VStack>
</CheckboxGroup>
)
}

MUI 5 Autocomplete filtered options count

How to get the count of the filtered options from MUI 5 Autocomplete component? (without changing the default behaviour of the prop filterOptions)
You can create a wrapper of filterOptions callback by using createFilterOption to create a default value like the one that is used internally and add your additional code to check for the result length:
import Typography from '#mui/material/Typography';
import TextField from '#mui/material/TextField';
import Autocomplete, { createFilterOptions } from '#mui/material/Autocomplete';
const _filterOptions = createFilterOptions();
export default function ComboBox() {
const [optionCount, setOptionCount] = React.useState(0);
const filterOptions = React.useCallback((options, state) => {
const results = _filterOptions(options, state);
if (optionCount !== results.length) {
setOptionCount(results.length);
}
return results;
}, []);
return (
<>
<Typography mb={2}>Option count: {optionCount}</Typography>
<Autocomplete
filterOptions={filterOptions}
options={top100Films}
sx={{ width: 300 }}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
</>
);
}
I was using the solution propsed by NearHuscarl and although it worked like a charm, there was this warning from react that kept popping up
Cannot update a component while rendering a different component
So what I did was to use a ref to store the count
const currentViewOptions = useRef(0);
// Instead of: setOptionCount(results.length);
currentViewOptions.current = results.length;
And it worked like a charm. In case anyone comes here with the same problem.

React component won't take onChange event

Obligatory "new to react" paragraph here. I have this rating component I got from material-ui and i'm trying to send the value to a database.
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Rating from '#material-ui/lab/Rating';
import Box from '#material-ui/core/Box';
const labels = {
0.5: 'Worst of the Worst',
1: 'Bad',
1.5: 'Poor',
2: 'Passable',
2.5: 'Ok',
3: 'Good',
3.5: 'Damn Good',
4: 'Great',
4.5: 'Love',
5: 'Perfection',
};
const useStyles = makeStyles({
root: {
width: 200,
display: 'flex',
alignItems: 'center',
},
});
export default function HoverRating(props) {
const [value, setValue] = React.useState(2);
const [hover, setHover] = React.useState(-1);
const classes = useStyles();
const onRatingChange = (event) => {
console.log(event.target.value)
props.reduxDispatch ({ type: "RATING_CHANGE", value: event.target.value
})
}
return (
<div className={classes.root}>
<Rating
name="hover-feedback"
value={value}
defaultValue={0}
precision={0.5}
size="large"
onChange={(event, newValue) => {
setValue(newValue);
console.log("your newValue is " + newValue)
}}
onChangeActive={(event, newHover) => {
setHover(newHover);
}}
{ onRatingChange }
/>
<br/>
{value !== null && <Box ml={2}>{labels[hover !== -1 ? hover : value]}</Box>}
</div>
);
}
It doesn't like something about my onRatingChange function. I've moved it all over the place and it's still throwing errors. I just really don't understand the issue. I'm mostly getting-
"./src/components/Rating.js
Line 54:11: Parsing error: Unexpected token, expected "..."
I've been at this for hours and I salvation.
Change your code from:
{ onRatingChange }
to:
onRatingChange={onRatingChange}
and change your file extension from .js to .jsx because you are using the JSX syntax
First, you appear the be storing the rating in two places: in your local state (with React.useState), and from the looks of your onRatingChange function, in a Redux store somewhere. It would be a good idea to pick one, and use that.
As for the direct answer to your question, your syntax is wrong. You're writing your Rating component in the following way:
<Rating
// ...
onChange={(event, newValue) => {
setValue(newValue);
console.log("your newValue is " + newValue)
}}
// ...
{ onRatingChange }
/>
The Rating component expects an onChange prop. I assume you want your onRatingChange function to be called when the rating changes. As such, you'd write:
<Rating
// ...
onChange={onRatingChange}
/>
The complication here though is that you're trying to register two different handlers for the rating change event. The bottom line is, decide on one, and then pass that as a callback function to the onChange prop.

Show loading state but also show previous results in React Concurrent gives a warning

UPDATE: Ok, it I misunderstood useDeferredValue, I thought it was more like a debounced value but it's not, you can define the timeout to be the time the old results will be shown.
So
const search = useDeferredValue(value, { timeoutMs: 10000 })
Gave me the desired effect, only it still show the warning right know.
Original
I want to have a search with the results below it, the search result should filter immediately based on the input of the text field. Then the query should be done debounced and the old results should show also when it takes less than e.g. 3000 m.s.
I'm working with the new concurrent mode in React and Relay experimental. I used the new useDeferredValue, documented on this page: https://reactjs.org/docs/concurrent-mode-reference.html#usetransition
But I got this warning:
Warning: Asynchronous triggered a user-blocking update that suspended.
The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern
I don't get this since it works but it still gives me a warning.
My code:
import React, {
Suspense,
useState,
// #ts-ignore - useDeferredValue does not exist yet in types
useDeferredValue,
// #ts-ignore - useDeferredValue does not exist yet in types
// useTransition,
useCallback,
ChangeEvent,
} from 'react'
import TextField from '#material-ui/core/TextField'
import LinearProgress from '#material-ui/core/LinearProgress'
import { graphql } from 'babel-plugin-relay/macro'
import { useLazyLoadQuery } from 'react-relay/hooks'
import {
FlowBlockFinderQuery,
FlowBlockFinderQueryResponse,
} from '../__generated__/FlowBlockFinderQuery.graphql'
import ErrorBoundaryWithRetry from '../helpers/ErrorBoundaryWithRetry'
interface RenderFuncProps {
search: string
filterSearch: string
}
function QueryResults({ search, filterSearch }: RenderFuncProps) {
const { blocks }: FlowBlockFinderQueryResponse = useLazyLoadQuery<
FlowBlockFinderQuery
>(
graphql`
query FlowBlockFinderQuery($search: String) {
blocks(search: $search) {
id
title
description
slug
blockType
}
}
`,
{ search },
{ fetchPolicy: 'store-or-network' }
)
return (
<div>
{blocks
.filter(
block =>
!filterSearch ||
block.title.toLowerCase().includes(filterSearch.toLowerCase())
)
.map(block => (
<div key={block.id} style={{ fontSize: 19 }}>
{block.title}
</div>
))}
</div>
)
}
function Results({ search, filterSearch }: RenderFuncProps) {
return (
<>
Zoekterm: {filterSearch}
<ErrorBoundaryWithRetry
fallback={({ error }) => <div>Er is iets foutgegaan</div>}
>
<Suspense fallback={<LinearProgress />}>
<QueryResults search={search} filterSearch={filterSearch} />
</Suspense>
</ErrorBoundaryWithRetry>
</>
)
}
export default function Asynchronous() {
const [value, setValue] = useState('')
// const [search, setSearch] = useState('')
const search = useDeferredValue(value, { timeoutMs: 3000 })
// const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG)
const onInputChange = useCallback(
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
// startTransition(() => {
setValue(event.currentTarget.value)
// })
},
[setValue]
)
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<TextField
label="Nieuw of bestaand blok"
fullWidth
variant="outlined"
value={value}
onChange={onInputChange}
/>
<br />
<Results search={search} filterSearch={value} />
</div>
)
}
React docs "if some state update causes a component to suspend, that state update should be wrapped in a transition". You have to make the async request suspense compatible and fetch the query in useTransition.
Here is an example from react docs
function handleChange(e) {
const value = e.target.value;
// Outside the transition (urgent)
setQuery(value);
startTransition(() => {
// Inside the transition (may be delayed)
setResource(fetchTranslation(value));
});
}
And the link to code sandbox

Resources