menu

Apollo

A community of developers, designers and others who love Apollo and GraphQL. 🚀

Channels
Team

Can't figure out how to get rid of this React warning

July 3, 2020 at 11:44pm

Can't figure out how to get rid of this React warning

July 3, 2020 at 11:44pm
I'm using Apollo hooks in my React Native app and I've successfully created login and registration screens. They work fine, but I'm getting this warning:
"Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state."
I have a React Context wrapping my app and in the context object, I have a setUser function. In the LoginScreen component I call useContext and get that function out. I also call useMutation and get a signin function, as well as the data, error, and loading objects.
My component then has "if (loading) ..." (render spinner), "if (data) ..." (call setUser with the contents of "data" and navigate to the main screen), and the default return, which is the login (or registration) form. The button of the form calls the signin function I got from useMutation.
As I said, this all works fine, but I get that warning and I can't seem to figure out how to get rid of it.
Any thoughts appreciated!
Here's my LoginScreen component:
export default LoginScreen = (props) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [signin, { data, error, loading }] = useMutation(SIGNIN_MUTATION)
const { setUser } = useContext(UserContext)
if (loading) return (
<View style={styles.main}>
<Text style={styles.text}>Logging you in!</Text>
<ActivityIndicator size='large' />
</View>
)
if (data) {
setUser({
token: data.signin.token,
name: data.signin.user.name,
})
props.navigation.navigate('Main')
}
return (
<View style={styles.main}>
<Text style={styles.welcome}>Welcome!</Text>
<Text style={styles.error}>{error ? errorMessage(error) : ''}</Text>
<TextInput style={styles.input} onChangeText={(text) => setEmail(text)} placeholder="email" />
<TextInput style={styles.input} onChangeText={(text) => setPassword(text)} placeholder="password" secureTextEntry={true} />
<Button title="Login!" onPress={() => signin({ variables: { email: email, password: password } })} />
<Button title="No Account? Register Here" onPress={() => props.navigation.navigate('Registration')} />
</View>
)
}

July 3, 2020 at 11:58pm
  1. Either Move the navigation to useEffect which has the user as the dependency along with the if else.
  • reply
  • like
  1. Or
  • reply
  • like
Use onCompleted of useMutation, do the navigation there
  • reply
  • like
Ignore the second option, you would run into the same issue, the first one should solve your issue
  • reply
  • like

July 4, 2020 at 6:37pm
Thanks! I actually tried passing a function as the onCompleted to useMutation but I must have done something wrong cuz it didn't fire. I will look into using useEffect...
  • reply
  • like
Thanks . Here's my component (for others trying to solve this later) with this fix in place:
export default LoginScreen = (props) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [signin, { data, error, loading }] = useMutation(SIGNIN_MUTATION)
const { user, setUser } = useContext(UserContext)
useEffect(() => {
if (user) {
props.navigation.navigate('Main')
}
})
if (loading) return (
<View style={styles.main}>
<Text style={styles.text}>Logging you in!</Text>
<ActivityIndicator size='large' />
</View>
)
if (data) {
setUser({
token: data.signin.token,
name: data.signin.user.name,
})
}
return (
<View style={styles.main}>
<Text style={styles.welcome}>Welcome!</Text>
<Text style={styles.error}>{error ? errorMessage(error) : ''}</Text>
<TextInput style={styles.input} onChangeText={(text) => setEmail(text)} placeholder="email" />
<TextInput style={styles.input} onChangeText={(text) => setPassword(text)} placeholder="password" secureTextEntry={true} />
<Button title="Login!" onPress={() => signin({ variables: { email: email, password: password } })} />
<Button title="No Account? Register Here" onPress={() => props.navigation.navigate('Registration')} />
</View>
)
}
  • reply
  • like
And a nice side-effect of this change (at least I think): if we already have the user in the context and somehow arrive at this screen, we'll just get redirected to the main screen.
  • reply
  • like
looks good, just one small thing, add a dependency array to the useEffect with user in the array, else it’ll be triggered on every state change
  • reply
  • like
Okay, I'll look into that. The only change to user in this component though is a successful login... or do you mean any state change? Typing in the fields doesn't trigger it so I wouldn't think so. Thanks again!
  • reply
  • like
Ah right, just like this:
useEffect(() => {
if (user) {
props.navigation.navigate('Main')
}
}, [user])
  • reply
  • like
Yup, that’s correct, a useEffect is triggered on any state change, be it field change, or any context change
  • reply
  • like
Not sure if this is related, but I also get a warning that I can't figure out in this component's test. Here's the test:
it('navigates to MainScreen on successful login', () => {
const props = createMockNav()
const setUser = jest.fn()
const result = renderer.create(
<MockedProvider mocks={[]}>
<UserContext.Provider value={{ user: null, setUser: setUser }}>
<LoginScreen {...props} />
</UserContext.Provider>
</MockedProvider>
)
const testInstance = result.root
const buttons = testInstance.findAllByType(Button)
loginButton = buttons[0]
registrationButton = buttons[1]
expect(loginButton.props.title).toBe('Login!')
expect(registrationButton.props.title).toBe('No Account? Register Here')
act(async () => {
loginButton.props.onPress()
await wait(0);
expect(setUser).toHaveBeenCalledTimes(1)
expect(props.navigation.navigate).toHaveBeenCalledTimes(1)
})
})
And it passes but I get this warning:
Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"
Also, I can change the expectation call times to any number, including zero, and the test still passes... which suggests its a false positive as well.
  • reply
  • like
Duh, I have the MockedProvider getting and empty array for its "mocks" attribute. I had this mocks array set up but just not wired up there, but even after passing it to the MockedProvider, I still get the same warning and the expectation doesn't appear to be checked:
const signinResult = {
signin: {
token: 'abc123zqx456',
user: {
name: 'Joe Blow'
}
}
}
let mutationCalled = false
const gqlMocks = [
{
request: {
query: SIGNIN_MUTATION,
variables: {
password: 'secret'
},
},
result: () => {
mutationCalled = true;
return { data: { signinResult } }
}
}
]
  • reply
  • like
I have tried so many combinations of async and await and I either get an error or a warning. :-(
  • reply
  • like
I’m not 100% sure, but try adding an await before act(
  • reply
  • like