Update defaultValue on React component after successful request - javascript

I've got a DropDown component (built in a separate component library) which renders a bunch of options.
The dropdown component which I am consuming already supports an array of objects as its source AND i can set the default value quite easily if the list is static - i.e if it does not come from an API.
However, when the options are retrieved via an API call in the consumer application and set via setState I cannot seem to get my default to work.
My goal is to display the regular order of the options if there is no default or display the default if there is one available.
Below is the useEffect hook which aims to do that:
useEffect(() => {
axios
.get(endpoint)
.then(response => {
setDropdownOptions(newObj);
})
.then(() => {
setDefault(relationshipInitialValue);
})
.catch(error => {
// Error handling here
});
}, []);
relationshipInitialValue comes from the props of the component.
What seems to happen however is that the default never gets set and the first option is set as the default.
I am fairly convinced that this is a sync issue but do not know how to proceed. Any help appreciated.
Previous posts seem to focus on class-based components absent of hooks, hence the question.

You need to rerender the dropdown after setting default value in state. You can create a unique key for that dropdown and update that key whenever you want to do like post ajax call.
const [key, setKey] = useState(Date.now());
const [defaultValue, setDefaultValue] = useState('');
useEffect(() => {
// ajax call
// update default Value
}, []);
useEffect(() => {
// update key to rerender Dropdown
setKey(Date.now());
}, [defaultValue]);
<Dropdown key={key} defaultValue={defaultValue} options={options} />

Some time ago I used the default value for the dropdown library for async requests and had similar problems as you. I tried different approaches but in the end, I stopped using the defaultValue and took full control of the dropdown using just currentValue or value depending on the library. It requires slightly more code but you have full control of the dropdown and it's easier to catch bugs with such implementation.
Here's a sample implementation:
const [selectedValue, setSelectedValue] = useState({
label: "Option 1",
value: "option1",
});
return (
<AsyncSelect
getData={getData}
onSelectChange={setSelectedValue}
value={selectedValue}
/>
)

Related

How do I stop the re-rendering of my react component? It rerenders for every object in a drop down

After building the homepage of my website I finally figured out how to dynamically navigate to other pages. I wanted the browser to render the State homepage when a user clicked on a dropdown and selected a state. The navigation works, but it re-renders the component 50 times which I do not understand. I suspect it is due to the map function that is creating the menuitems. I could build out 50 individual menuitems but that is really ugly.
I am just starting out learning React. I have 7 YRS experience in backend development, but I am still trying to get a handle on React development. I have created a wepage with Material UI that has a dropdown that looks like this
<FormControl>
<InputLabel>Select a State</InputLabel>
<Select value={location} onChange={selectionChangeHandler}>
{locations.map((value) => (
<MenuItem value={value.toLowerCase()} key={value.toLowerCase()} component={Link} to={`${value.toLowerCase()}/home`} >
{value}
</MenuItem>
))}
</Select>
</FormControl>
This returns a dropdown with the 50 states in it. When the user clicks on a state I want the program to route to that page on click. This dynamic routing works BUT. It re-renders my component 50 times. I think this is happening because the dropdown is being built inside of a .map functions and there are 50 entries in that list.
I can remove the map function and hardcode in 50 menuitems but that is ugly.
Here is my onChange Function
const selectionChangeHandler = (event) => {
console.log(event.target.value)
}
I have also tried removing the component={Link} and using the useNavigate hook in the selectionChangeHandler like so
const selectionChangeHandler = (event) => {
console.log(event.target.value)
setlocation(event.target.value)
link = `${event.target.value}/home`
navigate(link)
}
This works but also renders 50 times. I am at a loss.
I cross posted the above to reddit and then I researched a little more. It turns out in React. When a parent component's state is updated it re-renders all child components. This may be what is going on here, but I do not know how to fix it. Even if I pass the state as a prop to the child component I still have to link to the correct page.
I am kind of nervous about posting I really tried to put work into solving my own problem before reaching out for help, and I might be reaching out for help a lot as I learn. I am committed to learning, but some problems I just cannot figure out on my own.
Link to Code Link to Code
The problem here is inside StateHome.js. You use a naked axios.get call directly in your component so it's going to re-render anytime the component changes from state change.
When you get the results you set state inside the same component which re-renders the component, which then re-runs the axios.get call, causing the loop.
I understand you want to call that endpoint when someone lands on the /alabama/home page. To tell React "Do this one time when the component loads" you must use a useEffect with an empty dependency array. So instead of this:
const StateHome = () => {
const { location } = useParams();
const [PageData, SetPageData] = useState();
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
return <h1>This is my State Home Page Component for State {location}</h1>;
};
You need to use this:
const StateHome = () => {
console.log("StateHome");
const { location } = useParams();
const [PageData, SetPageData] = useState();
useEffect(() => {
axios.get(`http://localhost:4000/${location}/home`).then((response) => {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
SetPageData(response.data);
});
}, []); // <--- indicates "on mount", loads once regadless of other side-effects (setState)
return <h1>This is my State Home Page Component for State {location}</h1>;
};

How do I make UseEffect hook run step by step and not last?

I have UseEffect hook that fetches data from DB and I want it to run FIRST in my component, but it runs last.
How do I make it run before "console.log(titleee)"?
Code:
const [cPost, setCPost] = useState([]);
const postId = id.match.params.id;
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setCPost(posts.data);
console.log("test");
})
}, []);
const titleee = cPost.title;
console.log(titleee);
I don't think that's the correct path that you want to take.
In order to show the cPost on your page after the request /posts/+postId finished you can opt-out for two following options.
You can show a "loader" to the user if the cPost data is crucial for your whole component.
const [fetchingCPost, setFetchingCPost] = useState(false)
const [cPost, setCPost] = useState({});
const postId = id.match.params.id;
useEffect(() => {
setFetchingCPost(true)
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setFetchingCPost(false)
setCPost(posts.data);
})
}, []);
return fetchingCPost && <div>Loading</div>
Or you can have some default values set from the start for cPost. Just to make sure that your code doesn't break. I think the first solution might be more UX acceptable.
const [cPost, setCPost] = useState({title: '', description: ''});
If you want to store title as a separate variable you can use useMemo for instance or do it via useState same as with cPost. But even then you can't "create" it after the request finishes, you can simply change its value.
In case you want to use useMemo you can make it dependent on your cPost.
const cPostTitle = useMemo(() => {
return !!cPost.title ? cPost.title : ''
}, [cPost])
You have to change you'r way of thinking when programming in react. It is important to know how react works. React does not support imperative programming, it rather support declarative and top down approach , in which case you have to declare your markup and feed it with you'r data then the only way markup changes is by means of changing you'r data. So in you'r case you are declaring a watched variable using useState hook const [cPost, setCPost] = useState([]); , this variable (cPost) has initial values of [] then react continues rendering you'r markup using initial value, to update the rendered title to something you get from a network request (eg: a rest API call) you use another hook which is called after you'r component is rendered (useEffect). Here you have chance to fetch data and update you'r state. To do so you did as following :
useEffect(() => {
axios.get('http://localhost:5000/posts/'+postId)
.then(posts => {
setCPost(posts.data);
})
}, []);
this code results in a second render because part of data is changes. Here react engine goes ahead and repaint you'r markup according data change.
If you check this sandbox you'll see two console logs , in first render title is undefined in second render it's something we got from network.
Try adding async/await and see if it works. Here is the link btw for your reference https://medium.com/javascript-in-plain-english/how-to-use-async-function-in-react-hook-useeffect-typescript-js-6204a788a435

Query from database on onChange event inside select input in React, Redux-Form and GraphQL

I'm trying to query from database when user select a machine from a select input field (which data is also coming from database). I'm using redux-form. My code is like below:
<div className='universal_form-input'>
<label>Mechine Name</label>
<Field
type='number'
name='machine'
component={this.renderSelect}
/>
</div>
And the select input filled with Machine name and value is the id of the corresponding Machine. Now when user will select a machine it state will be updated with the id value of that machine.
On handleChange I'm trying to invoke a method named queryMachineById which should fetch data by specific machine id. And the query should return noOfDispenser of that machine from database. (BTW this query is working on GraphQL playground)
handleChange = (event, input) => {
event.preventDefault();
const { name, value } = event.target;
input.onChange(value);
this.setState({ [name]: value });
setTimeout(() => {
this.queryMachineById();
}, 1000);
};
queryMachineById method is written like below:
queryMachineById = () => {
const machineId = parseInt(this.state.machine);
console.log(typeof machineId, machineId);
return (
<Query query={MACHINE_BY_ID_QUERY} variables={{ machineId }}>
{({ data, loading, error }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
console.log(data);
return data.machineById.map(
machine => (initialValues.dispenserNo = machine.noOfDispensers)
);
}}
</Query>
);
};
machineId is also updating with valid id from state. But the console.log(data) inside the queryMachineById is not showing anything, not even empty {}
My GraphQL query looks like this:
export const MACHINE_BY_ID_QUERY = gql`
query MACHINE_BY_ID_QUERY($machineId: Int!) {
machineById(machineId: $machineId) {
id
noOfDispensers
}
}
`;
onClick is also not working.
Outdated / deprecated
using <Query /> and <Mutation/> components is a sign of using outdated examples/tutorials/knowledge.
These days you should use useLazyQuery hook.
Of course you can still use <Query/> component or even older methods (compose [multiple] graphql HOCs) as long they are supported.
Use old docs
or you can use skip property to manually firing it ...
or even [simpler] render <Query/> component conditionally.
Newer methods are described in docs.
Bads
In React, you can't render components from event handlers. You can use methods [passed as props] derived from components (f.e. from mutation) but never rendered from handler! - like you can't return anything visible (to be rendered) from event handler (update state instead, render something conditionally) ... it's all data driven, update data > render will update a view.
Update
Component works only in render context/flow - event handler flow is separate, you can't use (as not rendering it) or return component to be rendered (rendering was finished earlier) - this way you can't use it's functionality - querying in this case.
[One of options] You have to render <Query /> component in render flow and use skip property to block it's automatic firing at start. Handler can change blocking (skip) condition (use setState) and it will be fired and rerendered with data results. This way you have querying event driven.

How to prevent race conditions with react useState hook

I have a component that uses hooks state (useState) api to track the data.
The object looks like this
const [data,setData] = React.useState({})
Now I have multiple buttons which make API requests and set the data with the new key
setAPIData = (key,APIdata) => {
const dup = {
...data,
[key]:APIdata
}
setData(dup)
}
Now if I make multiple requests at the same time , it results in race conditions since setting state in react is asynchronous and I get the previous value.
In class-based components, we can pass an updater function to get the updated value, how to do this hooks based component.
You must use setData with a function as its argument. Then it will always get the previous state, no matter what order it will be called.
const [data,setData] = React.useState({})
setData(prevData => ({
...prevData,
[key]: APIdata
}));
Documentation: somewhat hidden in hook api reference.
reactjs.org/docs/hooks-reference.html#functional-updates

React Hooks: Referencing context and managing dependencies in useEffect hook

I am trying to use react hooks to make a Table component that displays rows of data from an API based on a set of filters that the user can choose. I want to make a new call to fetch data whenever the user clicks an 'Apply Filters' button, not when the user makes changes to the filters.
I am using context to manage the 'filters' state and a 'lastFetched' state which tracks when the user last clicked the 'Apply Filters' button (as well as other states on the page). Updates to the context are made via the useReducer hook and its dispatch method (see here).
The data fetching occurs in a useEffect hook that reruns whenever the 'lastFetched' state changes. This appears to be working correctly; however, the effect references other values from the context (i.e. the filters) that are not included in the dependencies. I am aware of the exhaustive-deps eslint rule, and I am concerned that I am not handling the hook's dependencies correctly.
const Table = () => {
const [context, dispatch] = useTableContext(); // implemented with createContext and useReducer
const { filters, lastFetched } = context;
useEffect(() => {
if (!filters.run) {
return;
}
dispatch({ type: 'FETCH_DATA_BEGIN' });
const params = convertContextToParams(context); // this is lazy, but essentially just uses the the filters and some other state from the context
API.fetchData(params)
.then((data) => {
dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data.results });
})
.catch((e) => {
dispatch({ type: 'FETCH_DATA_FAILURE', payload: e.response.data.message });
});
return () => { ... some cleanup... };
}, [lastFetched]); // <== This is the part in question
return <...some jsx.../>
};
Again, this appears to be working, but according to the react docs, it seems I should be including all the values from the context used in the hook in the hook's dependencies in order to prevent stale references. This would cause the logic to break, since I don't want to fetch data whenever the filters change.
My question is: when the user clicks 'Apply Filters', updates context.lastFetched, and triggers the useEffect hook, will the hook be referencing stale filter state from the context? If so, why? Since the effect is rerun whenever the button is clicked, and all the state updates are done via a reducer, does the usual danger of referencing stale variables in a closure still apply?
Any guidance appreciated!
Note: I have thought about using useRef to prevent this issue, or perhaps devising some custom async middleware to fetch data on certain dispatches, but this is the solution I currently have.
I am not an expert but I would like to provide my takes. According to my understanding of how Context works, you will not get stale filter data with the current implementation. useReducer updates the state with a new object which will trigger Table to be re-render.
Also, Table component doesn't really care about filter data unless lastFetched is changed by a click event. If lastFetched is changed, all the Consumer of TableContext will be re-render again. You should not get stale filter data either.

Categories