React Context - useEffect - javascript

Is it correct that a useEffect is listening to the context?
const {
data,
} = useContext(AuthContext);
async function getProfile() {
try {
setLoading(true);
console.info('getProfile called', data.token);
const response = await getByToken(data.token);
if (response && response.ok) {
setFirstName(response.userData.firstName);
setLastName(response.userData.lastName);
setEmail(response.userData.email);
}
} catch (e) {
console.info('Error', e);
} finally {
setLoading(false);
}
}
useEffect(() => {
if (data.userData) {
getProfile();
}
}, [data.userData]);
This is the way I found to make it make calls to getProfile with a userData different from null

Yes that is how it works. Although it might be incorrect to say that useEffect is listening to changes in the variable. The callback inside useEffect runs because of 2 conditions being fulfilled:
a render happens.
the dependency array has a changing dependency
The rerender is because the context value has changed and the inner component is a Consumer of the Context (cause it calls useContext).
And since the dependency included in the useEffect dependency array also changes, the callback is run.
A sandbox link with a simple example

Related

Why useState not apply new value

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.

Custom firebase functions in react app not returning value

I have made some custom firebase functions for my react app, so I don't have to write the whole code every time. The issue is that when there is a value to return (such as a user JSON), it doesn't return it when I called from another file.
Here is the piece of code not working:
Functions.js
import * as firebase from 'firebase'
const AuthState = () => {
firebase.auth().onAuthStateChanged(user => {
if (user) {
return user;
} else {
return null;
}
});
};
export {AuthState}
I call it in my React entry file:
App.js
import {AuthState} from './Functions'
class App extends Component {
componentDidMount() {
const result = AuthState();
console.log(result) // Undefined
}
...
I have tried to use normal functions rather than arrow functions but it doesn't fix the problem.
What's happening is that the firebase methods you're accessing are asynchronous, but your code ignores that and expects them to work synchronously. Essentially, the function will return while waiting for the async actions to resolve.
You call AuthState. The operations firebase.auth().onAuthStateChanged are fired, waiting for firebase to complete it's tasks and return. Before that happens, the rest of the lines in the function are executed. There are none, so an undefined is returned. Later, the callback passed to onAuthStateChanged is triggered, but is bound to nothing, so the resolved return values are unreachable.
To trigger some code once the operations have actually completed, you can change your code to use an async construct, either callbacks or promises.
callbacks:
const AuthState = (cb) => {
firebase.auth().onAuthStateChanged(user => {
if (user) {
cb(user);
} else {
cb(null);
}
});
};
Now, integrating the timing of an async operations with the render methods of React is a but more complicated. Don't put async code like that into the componentDidMount property. That can cause infinite update loops. Instead, either initialize in the constructor, or call when the user triggers it (i.e. a button click or pressing enter):
import {AuthState} from './Functions'
class App extends Component {
constructor(super) {
props(super)
this.state = {
result = null
}
// get the results of component render initialization:
AuthState(res => {
this.setState({
result: res
})
console.log(res)
});
// or you can wrap that in a function for attaching to DOM event listeners:
this.clickHandler = e => {
AuthState(res => {
this.setState({
result: res
})
console.log(res)
});
}
}
...

Why are these functions working different?

I'm trying to set the state of a property after making an API call only if the component hasn't been unmounted. In the first function the variable "unmounted" is initialize inside the function "Component"; in this case I'm getting this warning: "Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application."
In the second function I initialize the variable "unmounted" globally and in this case I'm not getting any warning.
Shows a warning:
function Component() {
const [emailSent, setEmailSent] = useState(false);
var unmounted = false;
async function handleClickEvent() {
try {
await AuthApi.sendRecoverAccountEmail('123');
!unmounted && setEmailSent(true);
} catch (err) {
!unmounted && setIsSendingEmail(false);
}
}
useEffect(() => {
return () => {
unmounted = true;
};
}, []);
}
No warnings:
var unmounted = false;
function Component() {
const [emailSent, setEmailSent] = useState(false);
async function handleSendEmail(formValues) {
try {
await AuthApi.sendRecoverAccountEmail('123');
!unmounted && setEmailSent(true);
} catch (err) {
!unmounted && setIsSendingEmail(false);
}
}
useEffect(() => {
return () => {
unmounted = true;
};
}, []);
}
Anyone can explain why is this happening?
On your first example, unmounted will always be false after each render.
Here's the right way without using an global instance:
function Component() {
const [emailSent, setEmailSent] = useState(false);
const unmounted = useRef(false);
async function handleSendEmail(formValues) {
try {
await AuthApi.sendRecoverAccountEmail('123');
!unmounted.current && setEmailSent(true);
} catch (err) {
!unmounted.current && setIsSendingEmail(false);
}
}
useEffect(() => {
return () => {
unmounted.current = true;
};
}, []);
}
An interesting question. You might find the FAQ about hooks useful, as I think it addresses your question rather specifically.
In your first example, your unmounted var is part of your component properties, where in the second example, it is just picked up as part of the javascript closure.
Usually, you want to make sure that you change the mounted parameter as part of the componentDidUnmount lifecycle method.
I think if you add unmounted into the list of dependencies, it might work? I normally use react from another framework in Clojurescript, so I'm not completely familiar with the semantics of the main javascript interface, but that would at least be why you're getting a warning for the first example.
In your first example, what happens if you change the last part to the following?
useEffect(() => {
return () => {
unmounted = true;
};
}, [unmounted]);
}
You may need to define unmounted as a more formal react property instead of just a property enclosed in your component.

Wait for fetch() to resolve using Jest for React test?

In my componentDidMount of a React.Component instance I have a fetch() call that on response calls setState.
I can mock out the request and respond using sinon but I don't know when fetch will have resolved it's promise chain.
componentDidMount() {
fetch(new Request('/blah'))
.then((response) => {
setState(() => {
return newState;
};
});
}
In my test using jest with enzyme:
it('has new behaviour from result of set state', () => {
let component = mount(<Component />);
requests.pop().respond(200);
component.update() // fetch() has not responded yet and
// thus setState has not been called yet
// so does nothing
assertNewBehaviour(); // fails
// now setState occurs after fetch() responds sometime after
});
Do I need to flush the Promise queue/callback queue or something similar? I could do a repeated check for newBehaviour with a timeout but that's less than ideal.
The best answer it seems is to be use a container pattern and pass down the API data from a container class with separated concerns and test the components separately. This allows the component under test to simply take the API data as props and makes it much more testable.
Since you're not making any real api calls or other time-consuming operations, the asynchronous operation will resolve in a predictably short time.
You can therefore simply wait a while.
it('has new behaviour from result of set state', (done) => {
let component = mount(<Component />);
requests.pop().respond(200);
setTimeout(() => {
try {
component.update();
assertNewBehaviour();
done();
} catch (error) {
done(error);
}
}, 1000);
});
The react testing library has a waitFor function that works perfectly for this case scenario.
I will give an example with hooks and function as that is the current react pattern. Lets say you have a component similar to this one:
export function TestingComponent(props: Props) {
const [banners, setBanners] = useState<MyType>([]);
React.useEffect(() => {
const response = await get("/...");
setBanners(response.banners);
}, []);
return (
{banners.length > 0 ? <Component> : </NoComponent>}
);
}
Now you can write a test like this to make sure that when banners are set Component is rendered
test("when the banner matches the url it renders", async () => {
const {container} = render(<TestingComponent />);
await waitFor(() => {expect(...).toBe(...)});
});
waitFor will wait for the condition in the function to be met before proceeding. There is a timeout that will fail the test if the condition is not met in X time. Check out the react testing library docs for more info

this.setState not working on componentWillMount()?

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
}
});
}

Categories

Resources