How to pass get parameter data to a component in react? - javascript

I am using react(next.js) to build a simple application. In this particular case, I'm trying to create a form in add and edit mode. If the get parameter contains edit, the form makes some api calls else it makes some different api calls. I'm passing the get paramaters as props but that doesn't seem to work.
Here's my code from the page:
const [formMode, setFormMode] = useState(h.form.FORM_MODE.ADD);
const [selectedContactId, setSelectedContactId] = useState("");
useEffect(() => {
const contact_id = h.findGetParameter("contact_id");
setSelectedContactId(contact_id);
const form_mode = h.findGetParameter("form_mode");
setFormMode(form_mode);
console.log(form_mode);
console.log(contact_id);
}, []);
return (
<div >
<Header />
<Body>
<div className="container projects-container">
<div className="mb-5 projects-title">
<h1> Create Link</h1>
</div>
<CreateMyForm
setLoading={setLoading}
formMode={formMode}
setFormMode={setFormMode}
selectedContactId={selectedContactId}
/>
</div>
</Body>
<Footer />
</div>
I'm taking get parameters contact_id and form_mode from the URL, setting them into in useState(which is working fine and being printed in console), and then passing them to the CreateMyForm component as props. It doesn't seem to be received in the component. Below is part of my code from the component which requires these parameters.
const { setLoading, formMode, setFormMode, selectedContactId } = props;
useEffect(() => {
// const contact_id = h.findGetParameter("contact_id");
// console.log("contact id = ", contact_id);
// setSelectedContactId(contact_id);
// const form_mode = h.findGetParameter("form_mode");
// setFormMode(form_mode);
// // console.log(contact_id);
// console.log("latest = ", form_mode);
// console.log("latest = ", selectedContactId);
(async () => {
if (h.cmpStr(formMode, h.form.FORM_MODE.EDIT)) {
const selectedContactRes = await api.contact.findById(
{ contact_id: selectedContactId },
{},
false
);
console.log("Her's me agains: ", selectedContactRes.data);
setSelectedContact(selectedContactRes.data.contact);
}
let projectApiRes = await api.project.contentFindAll({}, {}, false);
if (h.cmpStr(projectApiRes.status, "ok")) {
if (
projectApiRes.data.projects &&
projectApiRes.data.projects.length > 0
) {
let projects = handleProjectOptionList(projectApiRes.data.projects);
setProjects(projectApiRes.data.projects);
setProjectList(projects);
}
}
if (h.cmpStr(formMode, h.form.FORM_MODE.ADD)) {
let contactApiRes = await api.contact.findAll({}, {}, false);
if (h.cmpStr(contactApiRes.status, "ok")) {
if (
contactApiRes.data.contacts &&
contactApiRes.data.contacts.length > 0
) {
let contacts = handleContactOptionList(contactApiRes.data.contacts);
console.log("I am coming here");
setContactList(contacts);
}
}
}
})();
}, []);
I've even tried to try to capture the get parameters in the component but that doesn't seem to work either.
I'm guessing the problem is with the async API calls but I am not sure how to fix it. Any help please.
Thanks in advance

Your useEffect() function is running only when the component loads.
You have to call all the code inside of useEffect everytime contact_id or form_mode changes so I suggest you pass these as observable arguments to useEffect as follows
useEffect(()=>{
// All your runnable code
}, [contact_id, form_mode])
Now all your code inside will run each time contact_id or form_mode is even slightly changed
Also, I would recommend you to wrap all your awaits in a try-catch block so you can test out how well the API works

Related

React, conditional rendering wont register change in object

The if statement in canBookSlot() is only checked once for some reason. The second time canBookSlot() is triggered, the userDetailsObj.canBook should be 0 after running updateUser(). And according to the console log it is the case, but the if statement still runs, why?
let userDetailsString = localStorage.getItem("userDetails");
let userDetailsObj = JSON.parse(userDetailsString);
const updateUser = () => {
userDetailsObj["hasBooked"] = 1;
userDetailsObj["canBook"] = 0;
};
const canBookSlot = (id) => {
if (userDetailsObj.canBook != 0) { // always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
After each render userDetailsObj will take that value from localStorage. That's how every variable inside a component which isn't a state made with useState hook, or a ref made with useRef hook behaves. You can fix your problem this by using a state, like so:
const [userDetails, setUserDetails] = useState(JSON.parse(localStorage.getItem("userDetails")));
const updateUser = () => {
const newUserDetails = { ...userDetailsObj, hasBooked: 1, canBook: 0 };
setUserDetails(newUserDetails);
localStorage.setItem("userDetails", JSON.stringify(newUserDetails));
};
const canBookSlot = (id) => {
if (userDetails.canBook != 0) {
//Always true
updateUser();
Axios.post("http://localhost:3001/api/book/week1/ex", {
room: userDetailsObj.room,
id: id.id + 1,
}).then(() => updateData());
} else {
console.log("already booked");
}
};
Can you clarify where this code runs? Are you using a class component or functional component? Would you mind sharing the entire component? If it is doing what I think it is doing, the let userDetailsString = localStorage.getItem("userDetails"); is running every render which means on every render, it grabs the value in localStorage and uses that, rather than using your object stored in userDetailsObj.
If you are using functional components, you could fix this by using state.
let userDetailsString = localStorage.getItem("userDetails");
let [userDetailsObj, updateUserDetailObj] = useState(JSON.parse(userDetailsString));
const updateUser = () => {
let u = { ...userDetailsObj,
hasBooked: 1,
canBook: 0,
}
updateUserDetailObj(u);
};
If you are using class Components, let me know and I'll update it with that option.

Why do I see stale data even after invalidating my queries?

I have created a function which adds a specific item to my diary. 9/10 times everything works, which means that there is nothing wrong with the code?
However rarely I add the item to my diary, but I don't see the update values, even thought I activated queryClient.invalidateQueries() method, the value is updated on my server, because when I manually refresh I see the updated diary again.
Does this mean that by the time I activate invalidatequeries method, the update has not reached my server and that is why I am seeing stale data? But what would I do in that case?
Here is the function:
const newAddItemFunction = () => {
const day = newDiary?.[currentDay];
if (day && selectedMealNumber && selectedItem) {
setSavingItem(true);
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
router.push("/diary");
}
};
Here is my custom hook(useFirestoreQuery is just custom wrapped useQuery hook for firebase):
export const useGetCollectionDiary = () => {
const user = useAuthUser(["user"], auth);
const ref = collection(
firestore,
"currentDiary",
user.data?.uid ?? "_STUB_",
"days"
);
return useFirestoreQuery(
["currentDiary"],
ref,
{
subscribe: false,
},
{
select: (data) => {
let fullDaysArray = [] as Day[];
data.docs.map((docSnapshot) => {
const { id } = docSnapshot;
let data = docSnapshot.data() as Day;
data.documentId = id;
fullDaysArray.push(data);
});
fullDaysArray.sort((a, b) => a.order - b.order);
return fullDaysArray;
},
enabled: !!user.data?.uid,
}
);
};
NewAddItemToDiary function is just firebase call to set document:
//...json calculations
setDoc(
doc(
firestore,
"currentDiary",
auth.currentUser.uid,
"days",
day.documentId
),
newDiaryWithAddedItem
);
9/10 times everything works, which means that there is nothing wrong with the code?
It indicates to me that there is something wrong with the code that only manifests in edge cases like race conditions.
You haven't shared the code of what NewAddItemToDiary is doing, but I assume it's asynchronous code that fires off a mutation. If that is the case, it looks like you fire off the mutation, and then invalidate the query without waiting for the query to finish:
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
Mutations in react-query have callbacks like onSuccess or onSettled where you should be doing the invalidation, or, if you use mutateAsync, you can await the mutation and then invalidate. This is how all the examples in the docs are doing it:
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})

React Hook useEffect has missing dependencies - Case: Pagination

I have read several cases on stackoverflow regarding missing dependencies in useEffect:
example :
How to fix missing dependency warning when using useEffect React Hook?
Now the case I have is, I use useEffect for pagination:
Here's the source code:
react-router-dom configuration
{ path: "/note", name: "My Note", exact: true, Component: Note },
Note Component
const Note = (props) => {
const getQueryParams = () => {
return window.location.search.replace("?", "").split("&").reduce((r, e) => ((r[e.split("=")[0]] = decodeURIComponent(e.split("=")[1])), r),
{}
);
};
const MySwal = withReactContent(Swal);
const history = useHistory();
// Get Queries On URL
const { page: paramsPage, "per-page": paramsPerPage } = getQueryParams();
// Queries as state
const [queryPage, setQueryPage] = useState(
paramsPage === undefined ? 1 : paramsPage
);
const [queryPerPage, setQueryPerPage] = useState(
paramsPerPage === undefined ? 10 : paramsPerPage
);
// Hold Data Records as state
const [notes, setNotes] = useState({
loading: false,
data: [],
totalData: 0,
});
useEffect(() => {
console.log(queryPage, queryPerPage);
setNotes({
...notes,
loading: true,
});
// Fetching data from API
NoteDataService.getAll(queryPage, queryPerPage)
.then((response) => {
setNotes({
loading: false,
data: response.data,
totalData: parseInt(response.headers["x-pagination-total-count"]),
});
return true;
})
.catch((e) => {
MySwal.fire({
title: e.response.status + " | " + e.response.statusText,
text: e.response.data,
});
});
return false;
}, [queryPage, queryPerPage]);
const { loading, data, totalData } = notes;
...
So there are two problems here:
There is a warning React Hook use Effect has missing dependencies: 'MySwal' and 'notes'. Either include them or remove the dependency array. You can also do a functional update 'setNotes (n => ...)' if you only need 'notes' in the 'setNotes' call. If I add notes and MySwal as dependencies, it gives me a continuous loop.
When I access the "note" page, the Note component will be rendered.
Then, with pagination: / note? Page = 2 & per-page = 10, it went perfectly.
However, when returning to "/ note" the page does not experience a re-render.
Strangely, if a route like this / note? Page = 1 & per-page = 10, returns perfectly.
Does my useEffect not run after pagination?
First of all, move your API call inside of useEffect. After your data is fetched, then you can change the state.
useEffect(() => {
//Fetch the data here
//setState here
},[]) //if this array is empty, you make the api call just once, when the `component mounts`
Second Argument of useEffect is a dependancy array, if you don't pass it, your useEffect will trigger in every render and update, which is not good. If you parss an empty array, then it makes just one call, if you pass a value, then react renders only if the passed value is changed.

Testing React Components setState overload which takes a function

I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});

how to render returned data on prop change in react

I am having trouble figuring out how to get an API call to re-render to the screen. I have an apiCall function that passes this.state and changes the state via passed ref but it does not trigger a rerender on the props change.
searchBody.js
class SearchBody extends Component {
constructor(props) {
super(props)
const queryString = require('query-string');
const queryTerm = queryString.parse(this.props.location.search);
this.state = { urlSearchTerm: queryTerm.search,
searchTerm: '',
loaded: false,
buttonClicked: null,
apiData: [],
tableHeaders: [],
tableRows: []
}
// check if URL has search term if so pass term for apiCall
if (this.state.urlSearchTerm) {
this.state.searchTerm = this.state.urlSearchTerm
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
// capture input text field to state variable
handleChange = searchTerm => event => {
this.setState({ searchTerm: event.target.value })
//console.log(this.state.searchTerm)
}
// handle form submission
handleSubmit = (event) => {
console.log('Inside HandleSubmit')
console.log('button clicked update url to /?search=' + this.state.searchTerm)
this.props.history.push('/?search=' + this.state.searchTerm);
this.setState({buttonClicked: true})
event.preventDefault();
}
// load search from API if search term is in URL
componentDidMount() {
console.log('Inside compDidMount')
if (this.state.urlSearchTerm){
this.setState({apiData: apiCall(this.state)})
}
}
render() {
const { classes } = this.props;
let table = ''
//check if API has loaded data and show results if true
if (this.state.loaded){
if (this.state.apiData.length === 0 && this.state.buttonClicked){
table = 'No Results Found'
//reset search button State
this.setState({buttonClicked: false})
} else {
table = <TableData tableHead={this.state.tableHeaders} tableData={this.state.tableRows} />
//reset search button State
this.setState({buttonClicked: false})
}
}
return (
<Fragment>
<hr/>
<form /*className={classes.container}*/ noValidate autoComplete="off" onSubmit={this.handleSubmit} >
<TextField
id="search"
label="Search field"
type="search"
/* className={classes.textField}*/
margin="normal"
onChange={this.handleChange('search')}
/>
<Button color='primary' letiant="outlined" type="submit" >Search DB</Button>
</form>
<h1>Results: </h1>
{table}
</Fragment>
)
}
}
export default SearchBody
methods.js
// break API data into arry of data for table component rows.
export const parseTableHeaders = input => {
// console.log(input)
if (input !== undefined && input.length !== 0) {
let head = []
for(let key in input[0]){ head.push(key);}
//console.log(head)
return head
}
}
///break API data into array of headers for table component
export const parseTableRows = (input) => {
let rows = [];
for(let o in input) {
rows.push(Object.values(input[o]));
}
//console.log(head)
return rows
}
//get api data from AWS
export function apiCall(props) {
const searchTerm = props.searchTerm
let apigClientFactory = require('aws-api-gateway-client').default;
const config = {
//apiKey: 'xxxx',
invokeUrl:'https://xxxx.execute-api.us-east-2.amazonaws.com'
}
let apigClient = apigClientFactory.newClient(config);
let params = {
//This is where any header, path, or querystring request params go. The key is the parameter named as defined in the API
// userId: '1234',
search_keyword: searchTerm
};
// Template syntax follows url-template https://www.npmjs.com/package/url-template
let pathTemplate = '/beta/testDB'
let method = 'GET';
let additionalParams = {
//If there are any unmodeled query parameters or headers that need to be sent with the request you can add them here
headers: { },
queryParams: {
search_keyword: searchTerm
}
}
apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
//This is where you would put a success callback
console.log('apiCall Returned. searchTerm; ', searchTerm)
console.log(result)
props.loaded = true
props.tableHeaders = parseTableHeaders(JSON.parse(result.data))
props.tableRows = parseTableRows(JSON.parse(result.data))
return JSON.parse(result.data)
}).catch( function(result){
//This is where you would put an error callback
})
}
Am I structuring the code wrong? My understanding is that when a prop changes it will force a re-render. Should I pass the "this.state.apiData" into apiCall instead of the entire state like this?
apiCall(this.state.apiData)
This is running within componentDidMount() I believe this is the correct location to call an API, but it's not re-rendering upon callback. I can see in the debugger the state variables are updating as expected.
Should I set a return variable in apiCall() and have the return value update the state within the componentDidMount()? Would this force a re-render once the data is returned?
something like this?
this.setState({apiData: apiCall()})
If I return this.state.apiData from apiCall() and have it parse the table headers and rows inside apiCall, when the state variable is returned will this force an update?
You are running an Async Call to get some rest api Data. Async by definition means you have no idea when your code will finish. This means you will need some type of callback to run after your apiCall has finished.
What you have here is a rest api call that returns a promise object. The promise object is basically an interface for adding callbacks to asynchronous code. I recommend you take one of these options for running a callback after your restApi call.
1.) You can pass a callback function into restApi() as a second parameter. You would invoke this callback as such:
let that = this;
apiCall(props, function(result) {
that.setState({apiData: result});
});
export function apiCall(props, callback) {
...
apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
...
callback(result);
...
});
...
}
2.) Your other option would be to handle the resolution of your apiCall by latching onto the promise that is created by the api call. When you execute async code the call to the async method immediately returns the promise object which you can return to the calling function to allow the caller to attach callbacks. this may sound a bit confusing, I am not the greatest at explaining things but see the following:
let that = this;
apiCall(props).then(function(result) {
that.setState({apiData: result});
});
export function apiCall(props) {
...
return apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
...
});
}
The key difference here is that you are returning the actual async call to apigClient.invokeApi. this allows whoever calls apiCall() to attach any callback functionality in the .then() method.
Ultimately you want to make sure you are calling setState when the restApi data actually gets back to the caller. .then() is the easiest way to trigger that call and reliably get the returned result.
Note: you should also look into promises in JS as the .then method can accept 2 parameters, One function that handles the successful return of data and one that handles error reporting.
Eric Hasselbring helped me get to this answer.
I was calling apiCall() and it was not returning anything inside of componentDidMount. I added return before the methods.js apigClient.invokeAPI() so that it would return the result of the function
return apigClient.invokeApi(params, pathTemplate, method, additionalParams)
.then(function(result){
//This is where you would put a success callback
console.log('apiCall Returned. searchTerm; ', searchTerm)
console.log(result)
debugger
props.tableHeaders = parseTableHeaders(JSON.parse(result.data))
props.tableRows = parseTableRows(JSON.parse(result.data))
return JSON.parse(result.data)
}).catch( function(result){
//This is where you would put an error callback
})
then I updated my function call from within SearchBody.js to
componentDidMount() {
console.log('Inside compDidMount')
if (this.state.urlSearchTerm){
apiCall(this.state).then(apiResponse => this.setState({
loaded: true,
apiData: apiResponse
})
)
}
}
So now apiCall returns a result from a promise and the apiCall is now a promise because of the .then function added. this allows the state to be changed inside of componentDidMount so that react sees there was a prop change and re-renders.

Categories