I have built a function component, DepartmentSelect, that uses react-select Async to allow users to select a department from a dropdown. I want to pass the id of the selected department in the props.
I have a working example, but it seems like I have duplicated logic. I passed a promise to the loadOptions react-select prop and I store the options in the state. I have the departmentId of the selected department in the DepartmentSelect props and I am storing the selected value in the state.
//... imports and getOptionLabel removed for brevity
const getOptionValue = (option) => {
return option.id;
};
interface Props {
departmentId: string,
onChange: (number) => void
}
let options = [];
const DepartmentSelect = (props: Props) => {
const [value, setValue] = useState(
options.find(o => o.id == props.departmentId)
);
const [isLoading, setLoading] = useState(true);
const handleChange = (option, action) => {
const id = option && option.id;
setValue(options.find(o => o.id == id));
props.onChange(id);
};
const loadOptions = () => {
return ky.get('/sbm/departments.json')
.then(r => r.json())
.then(json => {
options = json;
setValue(options.find(o => o.id == props.departmentId));
setLoading(false);
return json;
});
};
return (
<AsyncSelect
defaultOptions
getOptionValue={getOptionValue}
loadOptions={loadOptions}
onChange={handleChange}
value={value}
/>
);
};
export default DepartmentSelect;
The code is working. I have removed a few irrelevant lines to make it shorter. Is there a way I can have the same functionality without storing the options and value in the state?
This question is very similar to
How can I work with just values in react-select onChange event and value prop?
In the above question, the options are passed in as a prop, so that they can get the selected prop without having to check if the options have been loaded from the server first. Would it be better to load the options separately and use a non-async select?
Edit
It would be nice if I could pass props.departmentId as the value prop to Async, but props.departmentId is a string. The value prop of Async requires one of the options, which are in the format
{
departmentId: string,
bill_cd: string,
name: string
}
Related
I apologize if I didn't phrase the question correctly so let me explain.
I have a reusable Filter component that generates checkboxes or radio buttons depending on a predetermined list.
For example, the lists below will generate each a Filter component with that many checkboxes and labels
const genre = ["Action", "Horror", "Comedy", "Drama"];
const contenttype = ["Series", "Movies", "Documentaries"];
const dategranularity = ["All Time", "Yearly", "Monthly", "Weekly"];
like so
<Filter data={genre} label="Genre" checkboxgroup />
<Filter data={contenttype} label="Content Type" checkboxgroup />
<Filter data={dategranularity} label="Date Granularity" defaultValue="all time" />
Here's the Filter component function I am using for my goal (reduced for brevity)
const Filter = ({ data, label, checkboxgroup, defaultValue }) => {
const [checkedState, setCheckedState] = useState([]);
const [radios, setRadios] = useState([]);
const [values, setValues] = useState([]);
const handleList = (e) => {
const updatedValuesState = values.includes(e.target.name)
? values.filter((label) => label !== e.target.name)
: [...values, e.target.name];
setValues(updatedValuesState);
setActive(updatedValuesState.length === data.length);
};
};
At the moment I able to capture and aggregate the checkboxes that are checked ONLY within its own Filter component So for example if I clicked on action and horror, I get values: ['action', 'horror'] in my console. That's good! But if I click on "series" on the next Filter component, the state is completely rebuilt and it console logs values: ['series'].
My end goal is to show values: ['action', 'horror', 'series'] even the radio button value can be aggregated but only once. as opposed to checkboxes can be multiple.
Here's a sandbox with all you need in place to mess with it. Thanks!!!
Every Filter component declared is its own instance. So genre and contenttype filter components won't be aware of each other.
However, we can declare a parent state which can be passed to all the filter components, although it will end up polluting the common Filter component.
I tried to do it, you can check it out here.
The problem is that each instance of the Filter component is an isolated instance, and each of them with has an isolated state (the states don't interact with each other). In this case the quickest solution, by following your code would be to do something like this:
const genre = ["Action", "Horror", "Comedy", "Drama"];
const contenttype = ["Series", "Movies", "Documentaries"];
const dategranularity = ["All Time", "Yearly", "Monthly", "Weekly"];
const Filter = ({ data, values, onChange, label, checkboxgroup, defaultValue }) => {
const [active, setActive] = useState(false);
const handleSwitch = (e) => {
if (e.target.checked) {
setActive(true);
onChange(data.map((item) => item.toLowerCase()));
} else {
setActive(false);
onChange([]);
}
};
// new unique Filter's onChange
const handleOnChange = (e) => {
let newValue = [];
if (checkboxgroup) {
const value = e.target.name;
newValue = values.includes(value)
? values.filter((label) => label !== value)
: [...values, value];
} else {
const value = e.target.value;
newValue = [value];
}
onChange(newValue);
setActive(newValue.length === data.length);
};
};
const Form = () => {
const [_genres, setGenres] = useState([]);
const [_contentType, setContentType] = useState([]);
const [_dateGranularity, setDateGranularity] = useState([]);
const handleAdvDrawerClose = () => {
const results = [..._genres, ..._contentType, ..._dateGranularity];
console.log("genres", _genres);
console.log("contentType", _contentType);
console.log("dateGranularity", _dateGranularity);
console.log("results", results);
};
return (
<div className="App">
<Filter
data={genre}
values={_genres}
onChange={setGenres}
label="Genre"
checkboxgroup
/>
<Filter
data={contenttype}
values={_contentType}
onChange={setContentType}
label="Content Type"
checkboxgroup
/>
<Filter
data={dategranularity}
values={_dateGranularity}
onChange={setDateGranularity}
label="Date Granularity"
defaultValue="all time"
/>
<Button variant="contained" size="small" onClick={handleAdvDrawerClose}>
Show Results
</Button>
</div>
);
}
Here is the link for the fork condesandbox
But basically what I did was simplify the onChange of the Filter, and the component now has 2 new props, the values and onChange, where the values is the selected values of the Filter and onChange is the onChange of the main component's state. And in the end where you click "Show Results" I merge all the states
I would like to use Ant Design AutoComplete component to fetch data from API. The component has the following code:
const Example = ({ token, service }) => {
const [value, setValue] = useState('')
const [options, setOptions] = useState([])
const { data } = useFetch(value)
const onSelect = (data) => {
console.log('onSelect', data)
}
const onChange = (query) => {
console.log("Search query ", query);
setValue(query);
console.log("State value ", value);
if (value && value.length > 1) {
setOptions(
data ? data : []
)
} else {
setOptions([])
}
}
return (
<AutoComplete
value={value}
options={options}
onSelect={onSelect}
onChange={onChange}
/>
)
}
data for the options is provided by useFetch(value) hook. So the value should be updated as the user types in the input.
But the problem is that value in the state is always one character behind the actual search query. Here is the link to my codesandbox. You can see that console.log() for search query and state value are always different. Is there any way to fix that? I need state value to be always in synch with the search query.
In this case you can make use of useEffect which can be used to calls API and update options.
You can do following:
React.useEffect(() => {
console.log("Value"); // This will be in sync with your search.
}, [value])
Working sandbox link: https://codesandbox.io/s/basic-usage-autocomplete-forked-eihbw?file=/index.js:369-448
I have a React component that fetches data using the useEffect hook like so:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
useEffect(() => {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}, [key, options])
return <p>{data}</p>;
}
This runs the hook every time key or options change. However, I'm also caching the data locally, and only want the effect to run when both key AND options change (since for each key/options combination the data will always be the same).
Is there a clean way to depend on the combination of key AND options rather than key OR options using React Hooks?
You can create this sort of logic with useRef(). Consider the following example and sandbox: https://codesandbox.io/s/react-hooks-useeffect-with-multiple-reqs-6ece5
const App = () => {
const [name, setName] = useState();
const [age, setAge] = useState();
const previousValues = useRef({ name, age });
useEffect(() => {
if (
previousValues.current.name !== name &&
previousValues.current.age !== age
) {
//your logic here
console.log(name + " " + age);
console.log(previousValues.current);
//then update the previousValues to be the current values
previousValues.current = { name, age };
}
});
return (
<div>
<input
placeholder="name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
placeholder="age"
value={age}
onChange={e => setAge(e.target.value)}
/>
</div>
);
};
Workflow:
We create a ref object for the two values we want to keep track of,
in this case its a name and age. The ref object is previousValues.
useEffect is defined but we do not provide it any dependencies.
Instead, we just have it execute whenever there is a state-change to
name or age.
Now inside useEffect we have conditional logic to check whether the
previous/initial values of both name and age are different than
their corresponding state-values. If they are then good we execute
our logic (console.log).
Lastly after executing the logic, update the ref object (previousValues) to the current values (state).
In order to run the effect when both values change, you need to make use of the previous values and compare them within the hook when either key or options change.
You can write a usePrevious hook and compare old and previous state as mentioned in this post:
How to compare oldValues and newValues on React Hooks useEffect?
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const previous = usePrevious({key, options});
useEffect(() => {
if(previous.key !== key && previous.options !== options) {
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
}
}, [key, options])
return <p>{data}</p>;
}
All provided solutions are perfectly fine, However there are some more complex situation e.g., When useEffect function should be called ONLY when dependency A and B changed while it also depends on C's value.
So I suggest using sequence of useEffects and intermediate States to provide more space for future logics. Implementation of this approach for asked question would be:
const cache = {key: "data-fetched-using-key"}
function Config({key, options}) {
const [data, setData] = useState();
const [needsUpdate, setNeedsUpdate] = useState(()=>({key:false, option:false}));
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, key:true}));
},[key])
useEffect(()=>{
setNeedsUpdate((needsUpdate)=>({...needsUpdate, options:true}));
},[options])
useEffect(() => {
if (needsUpdate.key && needsUpdate.options){
const fetchedData; // fetch data using key and options
setData(fetchedData);
cache[key] = fetchedData;
setNeedsUpdate(()=>({key:false, option:false}));
}
}, [needsUpdate, key, options])
return <p>{data}</p>;
}
In this way we can apply almost any logic on our useEffect dependencies, However it has own drawbacks which is few more rendering cycle.
You can create a new custom hook which calls the callback with an argument with index/names of dependencies
const useChangesEffect = (callback, dependencies, dependencyNames = null) => {
const prevValues = useRef(dependencies);
useEffect(() => {
const changes = [];
for (let i = 0; i < prevValues.current.length; i++) {
if (!shallowEqual(prevValues.current[i], dependencies[i])) {
changes.push(dependencyNames ? dependencyNames[i] : i);
}
}
callback(changes);
prevValues.current = dependencies;
}, dependencies);
};
useChangesEffect((changes) => {
if (changes.includes(0)) {
console.log('dep1 changed');
}
if (changes.includes(1)) {
console.log('dep2 changed');
}
}, [dep1, dep2]);
I have a list of warehouses that I pull from an API call. I then render a list of components that render checkboxes for each warehouse. I keep the state of the checkbox in an object (using the useState hook). when I check/uncheck the checkbox, I update the object accordingly.
My task is to display a message above the checkbox when it is unchecked. I tried simply using the object, however, the component was not re-rendering when the object changed.
I found a solution to my problem by simply adding another useState hook (boolean value) that serves as a toggle. Since adding it, the component re-renders and my object's value is read and acted on appropriately.
My question is: why did I have to add the toggle to get React to re-render the component? Am I not updating my object in a manner that allows React to see the change in state? Can someone explain to me what is going on here?
I've created a sandbox to demonstrate the issue: https://codesandbox.io/s/intelligent-bhabha-lk61n
function App() {
const warehouses = [
{
warehouseId: "CHI"
},
{
warehouseId: "DAL"
},
{
warehouseId: "MIA"
}
];
const [warehouseStatus, setWarehouseStatus] = useState({});
const [toggle, setToggle] = useState(true);
useEffect(() => {
if (warehouses.length > 0) {
const warehouseStates = warehouses.reduce((acc, item) => {
acc[item.warehouseId] = true;
return acc;
}, {});
setWarehouseStatus(warehouseStates);
}
}, [warehouses.length]);
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
setToggle(!toggle);
};
return warehouses.map((wh, idx) => {
return (
<div key={idx}>
{!warehouseStatus[wh.warehouseId] && <span>This is whack</span>}
<MyCheckbox
initialState
id={wh.warehouseId}
onCheckChanged={handleChange}
label={wh.warehouseId}
/>
</div>
);
});
}
Thanks in advance.
You are mutating state (don't mutate state)
this:
const handleChange = obj => {
const newState = warehouseStatus;
const { name, value } = obj;
newState[name] = value;
setWarehouseStatus(newState);
};
should be:
const handleChange = ({name,value}) => {
setWarehouseStatus({...warehouseStatus,[name]:value});
};
See the problem?
const newState = warehouseStatus; <- this isn't "newState", it's a reference to the existing state
const { name, value } = obj;
newState[name] = value; <- and now you've gone and mutated the existing state
You then call setState with the same state reference (directly mutated). React says, "hey, that's the same reference to the state I previously had, I don't need to do anything".
I have a Select custom component, that only have a select, options and listen for a onChange, then i have a useReducer code, that initialize with some variables, after select one option my state still have the initialized value
When i select ANOTHER the value in performSearch is ALL
const reducer = (state, newState) => ({ ...state, ...newState });
const [state, setState] = useReducer(reducer, {
filterStatus : 'ALL'
});
const performSearch = () => {
console.log(state.filterStatus) //<= first time is ALL, second time same value ALL, third, is another value
}
useEffect(() => {
performSearch()
},[])
<Select
onChange={(e) => {
const {value} = e.target
setState({filterStatus:value})
performSearch()
}}
items={[{key:"ALL",value:"ALL"},{key:"ANOTHER",value:"ANOTHER"}]}
/>
any idea?
If I had to guess I'd say that its because you are trying to call performSearch to log the console before the state is set. If you console log the state before you return your component you will probably be able to see the correct value in the state. I'm not sure what your use case is but if you want to use the value you can just return it in your function and not worry about the reducer and state at all. Like so:
const performSearch = (value) => {
console.log(value)
}
useEffect(() => {
performSearch('ALL')
},[])
<Select
onChange={(e) => {
const {value} = e.target
performSearch(value)
}}
items={[{key:"ALL",value:"ALL"},{key:"ANOTHER",value:"ANOTHER"}]}
/>
if you need to use the reducer then you can probably create a promise or I would just return the value to preformSearch and then set the state through your reducer from there like so:
const reducer = (state, newState) => ({ ...state, ...newState });
const [state, setState] = useReducer(reducer, {
filterStatus : 'ALL'
});
const performSearch = (value) => {
setState({filterStatus: value});
//Do your stuff with the value
console.log(value)
}
useEffect(() => {
//you can probably just set the value in preformSearch here manually or you can set it to the states value but setting to the states value would result in another render because you would set the state in performSearch again
performSearch(state.filterStatus)
},[])
<Select
onChange={(e) => {
const {value} = e.target
performSearch(value)
}}
items={[{key:"ALL",value:"ALL"},{key:"ANOTHER",value:"ANOTHER"}]}
/>
But like I said I'm not really sure what your end goal is with this component but for this use case I'm not sure you need to use the useReducer function at all.
The problem is that you are calling performSearch() inside the onChange function, so when you set the new state you will only see the value from the previous state. Try to put the performSearch function outside of the onSelect function and you will get the correct output.