Last week I was working on a task of adding multiple language support to the React Native app that is under developing. I used the package i18next as the backhone to support the feature. React Redux is also used to store the language choice as one of the app settings. During the development, I encountered an unexpected bug: The home screen does not re-render once I changed the language setting. It took me around 2.5 hours to resolve it, hence, I consider it worth a blog to write the think process and the solution down.

Context of the Issue

First, let us go through the context of the issue.

Store the Language Settings in React Redux

In the i18next, it has a global variable i18n.language to store language choice and is applied to the whole app. However, it is not enough, if we do not store the language setting, once the app is closed, the user choice will lost. Therefore, I used React Redux to store the language setting, as shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
const initialState = {
language: '',
}

export const appSettingSlice = createSlice({
name: 'appSettings',
initialState,
reducers: {
getLanguageSetting: (state) => {
return state.language
},
setLanguageSetting: (state, action) => {
if (!action.payload) return
state.language = action.payload
},
resetLanguageSetting: (state) => {
state.language = ''
},
},
})
...

The Expected Scenario

The project is structured as blow. The language in redux is set under set-language/index.jsx, along with i18n.language. And set-language is routed from my-account. What I expected is that if I change the language, once I switch back to the home screen, it should re-render and present with the changed language.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- root
-- app
-- (tabs)
-- home
...
-- index.jsx
...
-- my-account
...
-- set-language
-- index.jsx
-- index.style.js
...
-- index.js // entry
-- _layout.js
...
...
-- index.js
...

The Re-render Issue

Things did no go as I expected. Once I change the language setting and switch back to the home screen, it changes nothing, the scren still show the preivous language.

RCA (Root Cause Analysis)

I added a useEffct hook into (tabs)/home/index.jsx, and would like to see why the home screen does not re-render. The code is shown below:

1
2
3
4
5
useEffect(() => {
console.log(`language: ${language}`);
console.log(`i18n.language: ${i18n.language}`);
i18next.changeLanguage(i18n.language);
}, []);

Then I changed the language and i18n.language from the set-language again, I found out that as soon as I changed the variables, the logs were shown on the terminal (means that the useEffect hook in the home screen was triggered). When I switched back to the home screen, the terminal did not print out the logs again (means that useEffect hook was not trigered).

Hence, I reached to the conclusion that under the tabs navigation of Expo Router, the tabs are somehow binded together, i.e. change states in one tab, will cause the useEffect hooks in the other tabs triggered. In this case, the other tabs consider themselves are re-rendered already. For example, in my case, I changed the language state under my-account/set-language, the useEffect hook in the home screen is triggerd, since I am still in the set-language screen, the components in the home screen will not be re-endered. When I switch back to the home screen, as there is no state change, the useEffect hook will not be triggered, and the language in the home screen stay with the old one.

The Solution

After identifying the root cause, the thought for the solution is obvious: find a method to ‘force’ the (tabs)/home screen to be re-rendered when change the language state in the (tabs)/my-account/set-language screen.

Enlightened by the information provided from Device context doesn’t work with Expo Router, I use the i18n.language as the key value for the Stack component under app/_layout.js (which is the parent componet for (tabs) ). Therefore, everytime the i18n.language is changed, the key value of the Stack component of app/_layout.js will be changed, thus cause all tabs under the (tabs) directory be re-rendered. The source code is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...

const Layout = () => {
const { i18n } = useTranslation()

return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Stack key={JSON.stringify(i18n?.language)}>
<Stack.Screen name='(tabs)' options={{ headerShown: false }} />
</Stack>
</PersistGate>
</Provider>
)
}
...

Compile and run the app again. Bingo! The feature works as expcted :)

Comment and share

In the last few months, I am working on a Defi app which is based on the Expo platform. One of the ‘must have’ thing is that the app need to store insensitive user data locally, such as app settings. After comparing with several potential solutions, like react-native-mmkv (Not compatible with Expo), expo-sqlite (Not compatiblw with the Expo for web), redux-persist (build on top of Async Storage, compatible with Android, iOS and web, easy to use), I decided to adopt Redux.

I searched a bit regarding integrating Redux into Expo, however, in my Expo project, I used tabs navigation structure, so there is no app.js as the main entry (The file structure is listed blow). And all the solutions that I found only works on the Expo project that has an App component (normally app.js) as an entry point. After a few hours of trouble shooting, I finally found the solution myself and decided to share the method in this article, hopefully it will help someone in the future.

1
2
3
4
5
6
7
8
9
10
11
-- root
-- app
-- (tabs)
-- home
...
-- index.js // entry
-- _layout.js
...
...
-- index.js
...

Step by Step Guide

Although the way to the destination is rugged, the solution is quite simple. To embed Redux into an Expo project with the tabs navigation structure, we only need to add the redux provider into two files: app/index.js and app/_lauout.js.

app/index.js

As shown below, we wrap the <Redirect href="/home" /> with the Redux provider and the PersistGate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useRootNavigationState, Redirect } from 'expo-router';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
// store and persistor are self-defined common redux component, nothing special
import { store, persistor } from '../services/redux/store';

const Home = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Redirect href="/home" />
</PersistGate>
</Provider>
);
};

export default Home;

app/_layout.js

This is the tricky part, I could not image that to make Redux work, I need to wrap the Stack with the Redux provider and the PersistGate until I found out that this is a key step.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Stack } from 'expo-router';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/es/integration/react';
import { store, persistor } from '../services/redux/store';

const Layout = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
</PersistGate>
</Provider>
);
};

export default Layout;

After adding the two, I am able to use the Redux in the files under /app. Happy coding: )

Comment and share

  • page 1 of 1
Author's picture

Jingjie Jiang


Find a place I love the most