I want the state to be dependent on server data. I thought of using componentWillMount:
componentWillMount() {
this.setState( async ({getPublicTodosLength}, props) => {
const result = await this.getPublicTodosLengthForPagination();
console.log("result = ", result) // returns the length but not assigned on this.state.getPublicTodosLength
return { getPublicTodosLength: result+getPublicTodosLength }
});
}
getPublicTodosLengthForPagination = async () => { // get publicTodos length since we cannot get it declared on createPaginationContainer
const getPublicTodosLengthQueryText = `
query TodoListHomeQuery {# filename+Query
viewer {
publicTodos {
edges {
node {
id
}
}
}
}
}`
const getPublicTodosLengthQuery = { text: getPublicTodosLengthQueryText }
const result = await this.props.relay.environment._network.fetch(getPublicTodosLengthQuery, {})
return await result.data.viewer.publicTodos.edges.length;
}
There is value but it's not assigned on my getPublicTodosLength state? I think I don't have to bind here since result returns the data I wanted to assign on getPublicTodosLength state
Why not rather do something like this?
...
async componentWillMount() {
const getPublicTodosLength = this.state.getPublicTodosLength;
const result = await this.getPublicTodosLengthForPagination();
this.setState({
getPublicTodosLength: result+getPublicTodosLength,
});
}
...
It's simpler and easier to read. I think the problem with the original code is with using async function inside setState(). In transpiled code there is another wrapper function created and then it probably loose context.
If you want your state to be dependent on server data you should use componentDidMount().
componentWillMount() is invoked immediately before mounting occurs. It is called before render(), therefore setting state synchronously in this method will not trigger a re-rendering. Avoid introducing any side-effects or subscriptions in this method.
This is the only lifecycle hook called on server rendering. Generally, we recommend using the constructor() instead.
componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. If you need to load data from a remote endpoint, this is a good place to instantiate the network request. Setting state in this method will trigger a re-rendering.
From React Doc
May be you could use:
Code snippet:
(async ({getPublicTodosLength}, props) => {
const result = await this.getPublicTodosLengthForPagination();
console.log("result = ", result);
return { getPublicTodosLength: result + getPublicTodosLength }
})).then((v)=> this.setState(v));
Please let me know if that works.
i decided to make componentWillMount async and it worked well.
this is the code:
componentWillMount = async () => {
let result = await this.getPublicTodosLengthForPagination();
this.setState((prevState, props) => {
return {
getPublicTodosLength: result
}
});
}
Related
I have a React app built with the Minimal template and I'm trying to follow along with one of their tutorials, in order to create a Redux slice that feeds some data to a custom component. The data itself is collected from Firebase. Below is my code:
firebase.js - helper
export function getDocuments(col) {
const colRef = collection(db, col);
const q = query(colRef, where('uid', '==', auth.currentUser.uid));
getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
// return [1,2,3]
}
product.js - Redux slice
export function getProducts() {
return async (dispatch) => {
dispatch(slice.actions.startLoading());
try {
const products = await getDocuments('products');
dispatch(slice.actions.getProductsSuccess(products));
} catch (error) {
dispatch(slice.actions.hasError(error));
}
};
}
ProductList.js - component
const dispatch = useDispatch();
const { products } = useSelector((state) => state.client);
useEffect(() => {
dispatch(getProducts());
}, [dispatch]);
useEffect(() => {
if (products.length) {
// setTableData(products);
}
}, [products]);
If I console log data in the helper function (firebase.js), I get the values I expect, once the promise is resolved/fulfilled. However, if I console.log clients in the product.js slice or later in the component, I get undefined.
I assume my problem is not being able to understand how async + await + useEffect work together in order to fix this. My assumption is that I am trying to access the value before the promise is resolved and therefore before the helper function returns it. I confirmed that by returning a simple array [1, 2, 3] in my helper function as a test.
I think I am missing something fundamental here (I am not very experienced with React and JS in general and still learning things on the go). Can someone help me understand what am I doing wrong?
Thank you!
With await you can await the fulfillment or rejection of a promise, but your getDocuments Function does not return a promise. Change the last line of the function to the following:
return getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
Async and Await are no different in React than in plain JavaScript:
When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method
useEffect():
By using this Hook, you tell React that your component needs to do something after rendering. This function will run every time the component is re-rendered.
I use hook useState for set post value.
const [firstPost, setFirstPost] = useState();
useEffect(() => {
(async () => { await onFetchPosts(); })();
}, []);
const onFetchPosts = async () => {
try {
const { body } = await publicService.fetchPostById(119);
// get post
const post = body.posts;
if (post && post.postsId) {
console.log(`save...`, body.posts);
setFirstPost(body.posts);
}
console.log(`firstPost...`, firstPost);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
I dont understand, firstPost is not updated.
This is because setState calls are asynchronous. Read it here and here. As the per the doc I linked
setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall
Therefore, the state is usually not updated yet when you console.log it on the next line, but you can access/see the updated state on the next render. If you want to log values, you can put them as inside a <pre> tag in your HTML, or do console.log at the beginning, like below:
const [firstPost, setFirstPost] = useState();
// Console.log right on at the start of the render cycle
console.log("First post", firstPost);
useEffect(() => {
(async () => {
await onFetchPosts();
})();
}, []);
const onFetchPosts = async () => {
try {
const { body } = await publicService.fetchPostById(119);
// get post
const post = body.posts;
if (post && post.postsId) {
console.log(`save...`, body.posts);
setFirstPost(body.posts);
}
// Do not console.log the state here
// console.log(`firstPost...`, firstPost);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
// Can also debug like this
return <pre>{JSON.stringify(firstPost)}</pre>;
React may batch multiple setState() calls into a single update for performance.
Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
React does not change the state variables immediately when state is changed i.e why you are getting undefined in console.
However the new value of firstPost is assured to be there in the next render.
I have the following code:
const classes = useStyles();
const [data1, setData1] = useState([]);
const [searchedString, setSearchString] = useState("");
console.log(data1);
const fetchDataHandler = async () => {
setData1([]);
axios
.get(`http://localhost:5000/select?articul=${searchedString}`)
.then((response) => {
dataStruction(response.data);
})
.catch((err) => {
console.log(err);
});
};
const dataStruction = (data) => {
data.map((element1) => {
if (element1.secondaryArt.startsWith("30")) {
return setData1([...data1, { ...element1, level: 1 }]);
}
});
};
const onChangeSearchText = (event) => {
setSearchString(event.target.value);
};
I want whenever I call fetchDataHandler to be able to set data1 to empty array. Now it is working as that results are sticking every time I call fetchDataHandler. How can I do it?
Problem:
Your Asynchronous handler dataStruction closes over data1 before a new render is triggered at setData1([]); (at the top of your async function).
This happens because React state updates are batched and asynchronous.
Simple Solution:
If you get rid of (delete the line) setData1([]); and change setData1([...data1, { ...element1, level: 1 }]); to setData1([{ ...element1, level: 1 }]); then you will get an array with a new element in it without preserving the "old" elements.
Alternative Solution:
You can also wrap your state updates into functions like so:
Turn this: setState("foo")
Into this: setState((state, props) => "foo")
The second form (passing a function instead directly a state) ensures that the correct state is referenced. So in your case the second state update would reference the updated state.
How can I test the inner logic of the following method?
For example:
async method () {
this.isLoading = true;
await this.GET_OFFERS();
this.isLoading = false;
this.router.push("/somewhere");
}
So I have the method that toggles isLoading, calls an action, and routes somewhere. How can I be sure that isLoading was toggled correctly (true before action call and false after)?
You have to extract this.isLoading rows into a new method setLoading() and check if it was called.
The second argument of shallowMount/mount is the mounting options that could be used to override the component's data props upon mounting. This lets you pass in a setter that mocks the isLoading data prop, which then allows you to verify the property was modified within the method under test:
it('sets isLoading', () => {
const isLoadingSetter = jest.fn()
const wrapper = shallowMount(MyComponent, {
data() {
return {
// This setter is called for `this.isLoading = value` in the component.
set isLoading(value) {
isLoadingSetter(value)
}
}
}
})
//...
})
Then, you could use toHaveBeenCalledTimes() along with isLoadingSetter.mock.calls[] to examine the arguments of each call to the mocked setter. And since you want to test the effects of the async method, you'll have to await the method call before making any assertions:
it('sets isLoading', async () => {
//...
await wrapper.vm.method()
expect(isLoadingSetter).toHaveBeenCalledTimes(2)
expect(isLoadingSetter.mock.calls.[0][0]).toBe(true)
expect(isLoadingSetter.mock.calls.[1][0]).toBe(false)
})
If you also want to verify that GET_OFFERS() is called, you could use jest.spyOn() on the component's method before mounting:
it('gets offers', async () => {
const getOfferSpy = jest.spyOn(MyComponent.methods, 'GET_OFFERS')
const wrapper = shallowMount(MyComponent, /*...*/)
await wrapper.vm.method()
expect(getOfferSpy).toHaveBeenCalledTimes(1)
})
While I was working on a project, there some case that I have to change a state when user update a data.
Here is the code :
state = {
changedReservationStatus: toJS(this.props.reservationViewModel.getReservation())
}
reloadPageData = async () => {
const { reservationViewModel, reservationOrderViewModel } = this.props
const reservation = toJS(reservationViewModel.getReservation())
await reservationViewModel.fetchReservation(reservation.id).then(changedReservationStatus => this.setState({ changedReservationStatus }))
}
( reservation variable contains a data of an array. )
When user clicks save button, reloadPageData function works and then await reservationViewModel.fetchReservation starts to work, it triggers this.setState after .then statement.
But as you may know reservationViewModel.fetchReservation and this.setState both work asynchronously.
when I console.log this.state.changedReservationStatus inside render function, it renders the data I want to get.
But is it okay to use this.setState inside of Promise?
Will there be any issue?
Try the following solution .
state = {
changedReservationStatus: toJS(this.props.reservationViewModel.getReservation())
}
reloadPageData = async () => {
const { reservationViewModel, reservationOrderViewModel } = this.props
const reservation = toJS(reservationViewModel.getReservation())
await changedReservationStatus = reservationViewModel.fetchReservation(reservation.id);
this.setState({ changedReservationStatus });
}