How to access input value inside `transformErrors` - reactjs

When mapping errors inside transformErrors callback, I need to know the actual value of the input in question.
I need this to create a system for composing multiple existing formats into new composite formats. I want to match the input value against each of the "basic" formats and display the error for the one that fails. The allOf method of composing formats unfortunately doesn't work for me, for reasons very specific to my project.
I tried injecting the form data into my tranformErrors callback via currying and reading the data directly:
import _ from 'lodash'
import Form from '#rjsf/core'
const makeTransformErrors = formData => errors => {
errors.forEach(error => {
if (error.name === 'format') {
const value = _.get(formData, error.property)
// ...
}
})
}
const WrapedForm = (formData, ...rest) => {
const transformErrors = makeTransformErrors(formData)
return (
<Form
transformErrors={transformErrors}
formData={formData}
{...rest}
/>
)
}
but this way value lags one keystroke behind the actual state of the form, which is what I was expecting. Unfortunately this doesn't work even when I don't pass formData into makeTransformErrors directly, but instead I pass in an object containing formData and directly mutate it imeditately inside Forms onChange, which I was expecting to work.
What are other possible ways of accessing the field's value? Maybe it could be possible to configure (or patch) ajv validator to attatch the value to validation error's params?

Not sure exactly what kind of error validation you are trying todo, but have you tried using validate?
It can be passed as such :
<Form .... validate={validate} />
where validate is a function that takes as arguments formData and errors.
See documentation here

Ok, I found a way of achieving what I want, but it's so hacky I don't think I want to use it. I can get the up-to-date value when combining the above mentioned prop mutation trick with using a getter for the message, postponing the evaluation until the message is actually read, which happens to be enough:
import _ from 'lodash'
import Form from '#rjsf/core'
const makeTransformErrors = formDataRef => errors => {
return errors.map(error => {
if (error.name !== 'format') return error
return {
...error,
get message() {
const value = _.get(propPath, formDataRef.current) // WORKS! But at what cost...
}
}
})
}
const WrapedForm = (formData, onChange, ...rest) => {
const formDataRef = React.useRef(formData)
const transformErrors = makeTransformErrors(formDataRef)
handleChange = (params) => {
formDataRef.current = params.formData
onChange(params)
}
return (
<Form
transformErrors={transformErrors}
onChange={handleChange}
formData={formData}
{...rest}
/>
)
}

Related

Hello, on ReactJs, I loose focus on input when useState change value :

Here an example of the problem :
codesandbox.io
export default function App() {
const [hasInputChanged, setHasInputChanged] = useState(false);
let colorList = ["orange", "blue", "yellow"];
function handleChange(e) {
setHasInputChanged(true);
}
const MyLittleInput = () => {
return <input onChange={(e) => handleChange(e)} />;
};
return (
<>
{colorList.map((color) => (
<MyLittleInput key={color} />
))}
</>
);
}
I tried different solutions as defining Keys or using useRef but nothing worked
It's too much code to be debugged easily, but for what I can see on the fiddle, there are serveral things wrong, first of all you are doing really too much things for a simple increment/decrement of a input value. But most important you are defining theyr value using the parametresListe state, but never really changing it wit the setParametresListe function, which should be the only way to safely change controlled form inputs.
Just try to do a bit of cleaning on your code and to use the useState as it is meat to be used
Let us know any updates!
UPDATE:
Having a look at your cleaned code, the problem is that a input inside a component gets builded again and again.
The reason for that, is that each input should have they unique "key" prop, so react can easily understand what input is changed and update only that one.
You have 2 ways to make this work, for the first, I've edited your code:
import "./styles.css";
import React, { useState } from "react";
const DEFAULT_INPUT_STATE = {
orange: "",
blue: "",
yellow: ""
};
export default function App() {
let colorList = ["orange", "blue", "yellow"];
const [inputState, setInputState] = useState(DEFAULT_INPUT_STATE);
const handleChange = (e) => {
const { name, value } = e.target;
console.log(name);
setInputState({
...inputState,
[name]: value
});
};
return (
<>
{colorList.map((color, i) => (
<input
key={color}
name={color}
value={inputState[color]}
onChange={(e) => handleChange(e)}
/>
))}
</>
);
}
As you can see, I've just removed the component for the input and did a bit of other changes, but If you still want to use a component, you can moove all the .map function inside of it, but there's no way to create the input inside a component if it is in a .map function
There is too much code, difficult to follow through, in your example. In the nutshell, I see in dev tools, when I update an input, the entire example component is re-rendered, thus all input elements got destroyed and replaced by newly created ones, without focus. It must be just a bug in your code: once an input is updated it renders different stuff, instead of just changing the input value. But it is beyond something someone here would debug for you for free :D

Sorting data from an API (redux-toolkit)

I'm building a crypto app with react and redux-toolkit.
I'm trying to find a way to manage the data from the API. More specifically i want to be able to sort by value, volume, etc. and add an "isFavourite" property for each coin but i think (correct me if i'm wrong) that the only way to do this is by copying the data to another state. What i've tried so far was adding another state where i passed the data like this:
const [list, setList] = useState()
useEffect(() => {
setList(data)
}, [data])
//"const coinData = list?.data?.coins" instead of "const coinData = data?.data?.coins"
but then an error occured because the data on the "list" were "undefined".
The code bellow is the one that is running without any problems. How can i manage the API data? Am i on the right path or is there a more slick way to do what i want? Thank you!
function Main () {
const { data, error, isFetching } = useGetCryptosQuery()
if(isFetching) return 'Loading...'
const globalStats = data?.data?.stats
const coinData = data?.data?.coins
const coinList = coinData.map(coin => {
return (
<Coin
price = {coin.price}
key = {coin.uuid}
id = {coin.uuid}
name = {coin.name}
icon = {coin.iconUrl}
/>)
})
return (
<div>
<h2>Main</h2>
{coinList}
</div>
)
}
export default Main
You are on the right track - I set up something similar and added a check for null trying to map the data, and that avoids the error you probably got.
const coinList = coinData ? coinData.map((coin) => {
///...coin component
}) : <div></div>;
Then, instead of an error for undefined data, it will return an empty div - until the data is there, then it will render ok.

react-select Creatable: transforming created options

I trying to use react-select's Creatable select component to allow users to add multiple CORS Origins to be registered for my authentication server. I would like to be able to allow users to paste full URLs, and have these URLs be transformed into Origins (format: <protocol>://<origin>[:port]) once they are added to the Creatable select.
As an example, the user could paste http://some-domain.com:1234/management/clients?param1=abc&param2=123#fragment_stuff into the Creatable select, and this whole URL would automatically be converted/added as just its origin components: http://some-domain.com:1234.
This is a reduced version the component I've wrote (TypeScript):
import CreatableSelect from 'react-select/creatable';
...
type MyOptionType = {
label: string,
value: string,
}
function SomeComponent(props:{}) {
const [options, setOptions] = useState<MyOptionType[]>([]);
const onOptionsChanged = (newOptions: OptionsType<MyOptionType>) => {
// Get only options containing valid URLs, with valid origins
const validUrlsWithOrigins = newOptions.filter(option => {
try {
return !!(new URL(option.value).origin);
} catch (error) {
return false;
}
});
// Transform options (e.g.: "http://some-domain.com:1234/abc?def=ghi#jkl" will become "http://some-domain.com:1234")
const newOptionsOrigins = validUrlsWithOrigins
.map(option => new URL(option.value).origin)
.map(origin => ({label: origin, value: origin}));
setOptions(newOptionsOrigins);
}
return <CreatableSelect isMulti options={options} onChange={onOptionsChanged} />
}
While debugging using React Developer Tools, I can see that the state of my component is being transformed accordingly, having only the origin part of my URLs being kept in the state:
The problem is that the Creatable select component is rendering the full URL instead of only the URL's Origin:
Why isn't the Creatable select in sync with the component's state? Is there a way to solve this, or is it a limitation on react-select?
You need to distinguish two things here - options prop of CreatableSelect holds an array of all the possibilites. But the value of this component is managed by value property.
You can check Multi-select text input example on docs page but basically you'll need to:
keep values and option separetly:
const [options, setOptions] = React.useState<MyOptionType[]>([]);
const [value, setValue] = React.useState<MyOptionType[]>([]);
const createOption = (label: string) => ({
label,
value: label
});
<CreatableSelect
isMulti
options={options}
value={options}
onChange={onOptionsChanged}
/>
and modify your onOptionsChanged function
set value of transformed and validated input
add new options to options state variable (all options, without duplicates)
Here's some example:
// Transform options (e.g.: "http://some-domain.com:1234/abc?def=ghi#jkl" will become "http://some-domain.com:1234")
const newOptionsOrigins = validUrlsWithOrigins
.map((option) => new URL(option.value).origin)
.map((origin) => createOption(origin));
setValue(newOptionsOrigins);
//get all options without duplicates
const allUniqueOptions: object = {};
[...newOptionsOrigins, ...options].forEach((option) => {
allUniqueOptions[option.value] = option.value;
});
setOptions(
Object.keys(allUniqueOptions).map((option) => createOption(option))
);
};

How Can I Setup `react-select` to work correctly with server-side data by using AsyncSelect?

I would like to setup a component react-select to work server-side data and do server-side filtering, but it doesn't work for a plethora of reasons.
Can you explain it and also show working code?
react-select has several examples in the documentation including an entire section dedicated to AsyncSelect which include inline code examples with codesandbox links.
It's worth noting that there are three unique props specific to the AsyncSelect
loadOptions
defaultOptions
cacheOptions
The primary difference between AsyncSelect and Select is that a Select is reliant on an options prop (an array of options) whereas the AsyncSelect is instead reliant on a loadOptions prop (an async function which provides a callback to set the options from an api).
Often api autocomplete lookups filter results on the server so the callback on the loadOptions does not make assumptions on filtering the results returned which is why they may need to be filtered client-side prior to passing them to the AsyncSelect state.
Here is a simple code example.
import React from 'react';
import AsyncSelect from 'react-select/async';
const filterOptions = (options, inputValue) => {
const candidate = inputValue.toLowerCase();
return options.filter(({ label }) => label.toLowerCase().includes(candidate);
};
const loadOptions = (inputValue, callback) => {
const url = `www.your-api.com/?inputValue=${inputValue}`;
fetch(url).then(resp => {
const toSelectOption = ({ id, name }) => ({ label: name, value: id });
// map server data to options
const asyncOptions = resp.results.map(toSelectOption);
// Filter options if needed
const filtered = filterOptions(asyncOptions, inputValue);
// Call callback with mapped and filtered options
callback(filtered);
})
};
const AsyncLookup = props => (
<AsyncSelect
cacheOptions
loadOptions={loadOptions}
defaultOptions
{...props}
/>
);
export default AsyncLookup
Let's start by me expressing the opinion that react-select seems great, but not very clearly documented. Personally I didn't fall in love with the documentation for the following reasons:
No search
All the props and put on a single page. If I do CTRL+F on something everything lights up. Pretty useless
Most descriptions are minimal and not describing the important edge cases, some are even missing
There are some examples, but not nearly enough to show the different varieties, so you have to do guesswork
And so I will try to help a bit with this article, by giving steps by steps, code and problems + solutions.
Step 1: Simplest form react-select:
const [options, setOptions] = useState([
{ id: 'b72a1060-a472-4355-87d4-4c82a257b8b8', name: 'illy' },
{ id: 'c166c9c8-a245-48f8-abf0-0fa8e8b934d2', name: 'Whiskas' },
{ id: 'cb612d76-a59e-4fba-8085-c9682ba2818c', name: 'KitKat' },
]);
<Select
defaultValue={options[0]}
isClearable
options={options}
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
It generally works, but you will notice that if I type the letter d which doesn't match any of the choices anywhere, choices stay, instead of showing "no options" as it should.
I will ignore this issue, since it is minor and seems unfixable.
So far so good, we can live with that small issue.
Step 2: Convert static data to server data
Our goal is now to simply swap the static data with server loaded data. Meh, how difficult could it be?
We will first need to swap <Select/> for <AsyncSelect/>. Now how do we load data?
So looking at the documentation there are multiple ways of loading data:
defaultOptions: The default set of options to show before the user starts searching. When set to true, the results for loadOptions('') will be autoloaded.
and
loadOptions: Function that returns a promise, which is the set of options to be used once the promise resolves.
Reading it carefully you understand defaultOptions needs to be a boolean value true and loadOptions should have a function returning the choices:
<AsyncSelect
defaultValue={options[0]}
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultOptions
loadOptions={loadData}
/>
Looks great, we have remote data loaded. But we want to preset our default value now. We have to match it by Id, rather than choosing the first one. Here comes our first problem:
PROBLEM: You can't set the defaultValue in the very beginning, because you have no data to match it against. And if you try to set the defaultValue after component has loaded, then it doesn't work.
To solve that, we need to load data in advance, match the initial value we have, and once we have both of those, we can initialize the component. A bit ugly but that's the only way I could figure it out given the limitations:
const [data, setData] = useState(null);
const [initialObject, setInitialObject] = useState(null);
const getInitial = async () => {
// make your request, once you receive data:
// Set initial object
const init= res.data.find((item)=>item.id=ourInitialId);
setInitialObject(init);
// Set data so component initializes
setData(res.data);
};
useEffect(() => {
getInitial();
}, []);
return (
<>
{data!== null && initialObject !== null ? (
<AsyncSelect
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
// loadOptions={loadData} // we don't need this anymore
/>
) : null}
</>
)
Since we are loading the data ourselves, we don't need loadOptions so we will take it out. So far so good.
Step 3: Make filter with server-side filtering call
So now we need a callback that we can use for getting data. Let's look back at the documentation:
onChange: (no description, from section "StateManager Props")
onInputChange: Same behaviour as for Select
So we listen to documentation and go back to "Select Props" section to find:
onInputChange: Handle change events on the input`
Insightful...NOT.
We see a function types definition that seems to have some clues:
I figured, that string must by my text/query. And apparently it drops in the type of change. Off we go --
const [data, setData] = useState(null);
const [initialObject, setInitialObject] = useState(null);
const getInitial = async () => {
// make your request, once you receive data:
// Set initial object
const init= res.data.find((item)=>item.id=ourInitialId);
setInitialObject(init);
// Set data so component initializes
setData(res.data);
};
useEffect(() => {
getInitial();
}, []);
const loadData = async (query) => {
// fetch your data, using `query`
return res.data;
};
return (
<>
{data!== null && initialObject !== null ? (
<AsyncSelect
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
onInputChange={loadData} // +
/>
) : null}
</>
)
Data gets fetched with the right query, but options don't update as per our server data results. We can't update the defaultOptions since it is only used during initialization, so the only way to go would be to bring back loadOptions. But once we do, we have 2 calls on every keystroke. Blak. By countless hours and miracle of painstaking experimentation, we now figure out that:
USEFUL REVELATION: loadOptions actually fires on inputChange, so we don't actually need onInputChange.
<AsyncSelect
isClearable
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
// onInputChange={loadData} // remove that
loadOptions={loadData} // add back that
/>
Things look good. Even our d search has automagically been fixed somehow:
Step 4: Update formik or whatever form value you have
To do that we need something that fires on select:
onChange: (no explanation or description)
Insightful...NOT. We have a pretty and colorful definition again to our rescue and we pick up some clues:
So we see the first param (which we don't know what it is can be object, array of array, null, or undefined. And then we have the types of actions. So with some guessing we figure out, it must be passing the selected object:
We will pass setFieldValue function as a prop to the component:
onChange={(selectedItem) => {
setFieldValue(fieldName, selectedItem?.id); // fieldName is also passed as a prop
}}
NOTE: careful, if you clear the select it will pass null for selectedItem and your JS will explode for looking for .id of undefined. Either use optional chaining or as in my case set it conditionally to '' (empty string so formik works).
Step 5: Final code:
And so we are all set with a fully functional reusable Autocomplete dropdown select server-fetching async filtering, clearable thingy.
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import AsyncSelect from 'react-select/async';
export default function AutocompleteComponent({
fieldName,
initialValue,
setFieldValue,
getOptionLabel,
queryField,
}) {
const [options, setOptions] = useState(null);
const [initialObject, setInitialObject] = useState(null);
// this function only finds the item from all the data that has the same id
// that comes from the parent component (my case - formik initial)
const findByValue = (fullData, specificValue) => {
return fullData.find((e) => e.id === specificValue);
};
const loadData = async (query) => {
// load your data using query HERE
return res.data;
};
const getInitial = async () => {
// load your data using query HERE
const fetchedData = res.data;
// match by id your initial value
const initialItem = findByValue(fetchedData, initialValue);
// Set both initialItem and data options so component is initialized
setInitialObject(initialItem);
setOptions(fetchedData);
}
};
// Hit this once in the beginning
useEffect(() => {
getInitial();
}, []);
return (
<>
{options !== null && initialObject !== null ? (
<AsyncSelect
isClearable
getOptionLabel={getOptionLabel}
getOptionValue={(option) => option.id}
defaultValue={initialObject}
defaultOptions={options}
loadOptions={loadData}
onChange={(selectedItem) => {
const val = (selectedItem === null?'':selectedItem?.id);
setFieldValue(fieldName, val)
}}
/>
) : null}
</>
);
}
AutocompleteComponent.propTypes = {
fieldName: PropTypes.string.isRequired,
initialValue: PropTypes.string,
setFieldValue: PropTypes.func.isRequired,
getOptionLabel: PropTypes.func.isRequired,
queryField: PropTypes.string.isRequired,
};
AutocompleteComponent.defaultProps = {
initialValue: '',
};
I hope this saves you some time.

Why toggleSelection and isSelected method are receiving different key parameter?

I am using react-table in version 6.10.0. with typescript.
There is an easy way to add checkbox with hoc/selectTable
However toggleSelection an isSelected method you need to provide to manage selection are receiving different key.
toggleSelection method is receiving extra "select-" at the beginning.
I could not found any example which such a problem.
I know there is a simple workaround for this problem, but still I could not found any example which extra string at the beginning. I am new in react and it seems that I do it incorrectly.
import "bootstrap/dist/css/bootstrap.min.css";
import ReactTable, { RowInfo } from "react-table";
import "react-table/react-table.css";
import checkboxHOC, { SelectType } from "react-table/lib/hoc/selectTable";
const CheckboxTable = checkboxHOC(ReactTable);
....
render() {
...
<CheckboxTable
data={this.getData()}
columns={this.columnDefinitions()}
multiSort={false}
toggleSelection={(r,t,v) => this.toggleSelection(r,t,v)}
isSelected={(key)=> this.isSelected(key)}
/>
}
...
toggleSelection = (key: string, shiftKeyPressed: boolean, row: any): any => {
...
//implementation -over here key is always like "select-" + _id
...}
isSelected = (key: string): boolean => {
// key received here is only _id
return this.state.selection.includes(key);
}
In all examples I have seen the methods are provided with the same key.
Looking at the source, it seems like it's working as intended, or there's a bug. If you haven't found any other mention of this, it's probably the former.
This is where the SelectInputComponents are created:
rowSelector(row) {
if (!row || !row.hasOwnProperty(this.props.keyField)) return null
const { toggleSelection, selectType, keyField } = this.props
const checked = this.props.isSelected(row[this.props.keyField])
const inputProps = {
checked,
onClick: toggleSelection,
selectType,
row,
id: `select-${row[keyField]}`
}
return React.createElement(this.props.SelectInputComponent, inputProps)
}
The two handlers of interest are onClick (which maps to toggleSelection) and checked, which maps to isSelected. Notice the id here.
The SelectInputComponent looks like this:
const defaultSelectInputComponent = props => {
return (
<input
type={props.selectType || 'checkbox'}
aria-label={`${props.checked ? 'Un-select':'Select'} row with id:${props.id}` }
checked={props.checked}
id={props.id}
onClick={e => {
const { shiftKey } = e
e.stopPropagation()
props.onClick(props.id, shiftKey, props.row)
}}
onChange={() => {}}
/>
)
In the onClick (i.e. toggleSelection) handler, you can see that props.id is passed in as the first argument. So this is where the additional select- is being added.
I'm not familiar with this package so I can't tell you if it's a bug or a feature, but there is a difference in how these callback arguments are being passed. Due to the maturity of the package, it strongly suggests to me that this is intended behaviour.

Resources