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

  • page 1 of 1
Author's picture

Jingjie Jiang


Find a place I love the most