In my Expo React Native application, I have a context variable (trackingAsked) that is also a state variable that I use to display a different opening screen. Everything works fine. On power up, the question hasn't been asked and the asking screen comes up. Once asked the context variable is set and stored in storage and it never asks it again. Until you want to change your selection in a Settings screen. Then I set the variable to false and the asking screen comes up but I can never set the variable to true.
I instrumented the opening screen and it appeared to be some sort of race condition. But then it was pointed out to me that it was a state variable which I couldn't test the way I was doing it.
I have created a similar scenario with a snack. I copied bits and pieces of my apps code hoping it would create the same scenario. And it did.
https://snack.expo.dev/@rjapenga/usecontext-example
The code you are interested in is in home.js. (but perhaps my problem is in App.js). Ignore the console.logs in the Snack since I now know why those are wrong.
I have three buttons:
Opt In - sets tracking true and asking true
Opt Out - sets tracking false and asking false
Clear Asking Variable
I display these two variables trackingAsked and trackingAllowed on the home.js screen.
I print out a log of the variables after you push one of the buttons. But that was foolish - since they are state variables which change later.
It was my assumption, that a context variable that was also a state variable would force a re-render when it was changed. But it does not.
So the basic question remains: "Why do config.trackingAsked and config.trackingAllowed not change on the screen since they are state variable?
Here are some code snippets: Here is the home screen component:
export function HomeScreen() {
useEffect(() => {
async function prepare() {
let askedBoolean = false;
let allowedBoolean = true;
try {
let asked = await retrieveData('TrackingAsked');
if (asked === 'true') {
askedBoolean = true;
} else {
askedBoolean = false;
}
} catch {
askedBoolean = false;
}
try {
let allowed = await retrieveData('TrackingAllowed');
if (allowed === 'true') {
allowedBoolean = true;
} else {
allowedBoolean = false;
}
} catch {
allowedBoolean = true;
}
setConfig({ ...config, trackingAsked: askedBoolean });
setConfig({ ...config, trackingAllowed: allowedBoolean });
}
prepare();
});
const { config, setConfig } = useContext(ConfigContext);
const disableTracking = async () => {
console.log('Tracking Asked = ', config.trackingAsked);
console.log('Tracking Allowed', config.trackingAllowed);
await storeData('TrackingAsked', 'true');
await storeData('TrackingAllowed', 'false');
setConfig({ ...config, trackingAsked: true });
setConfig({ ...config, trackingAllowed: false });
};
const enableTracking = async () => {
console.log('Tracking Asked = ', config.trackingAsked);
console.log('Tracking Allowed', config.trackingAllowed);
await storeData('TrackingAsked', 'true');
await storeData('TrackingAllowed', 'true');
setConfig({ ...config, trackingAsked: true });
setConfig({ ...config, trackingAllowed: true });
};
const clearAsk = async () => {
console.log('Tracking Asked = ', config.trackingAsked);
await storeData('TrackingAsked', 'false');
setConfig({ ...config, trackingAsked: false });
};
return (
<ScrollView>
<AccountContainer>
<Title>Sermon Engagements</Title>
<Spacer />
<Text1>
This app records data analytics about how the app is used.
</Text1>
<Spacer />
<Text1>Tracking Asked {JSON.stringify(config.trackingAsked)}</Text1>
<Text1>Tracking Allowed {JSON.stringify(config.trackingAllowed)}</Text1>
</AccountContainer>
<Spacer />
<AuthButton
mode="contained"
icon="flashlight"
hitSlop={{ top: 15, bottom: 15, left: 15, right: 15 }}
onPress={() => enableTracking()}>
Allow Data Analytics
</AuthButton>
<Spacer />
<AuthButton
mode="contained"
icon="flashlight-off"
hitSlop={{ top: 15, bottom: 15, left: 15, right: 15 }}
onPress={() => disableTracking()}>
Opt Out of Data Analytics
</AuthButton>
<Spacer />
<AuthButton
mode="contained"
icon="flashlight-off"
hitSlop={{ top: 15, bottom: 15, left: 15, right: 15 }}
onPress={() => clearAsk()}>
Clear Asked
</AuthButton>
</ScrollView>
);
}
Here is the App component
export default function App() {
const [config, setConfig] = useState({
playAll: false, // Play all Engagements of One Sermon
playAllSermons: false, // Play all Engagements of All Sermons
playMusic: false,
autoPlay: true,
musicVolume: 0.1,
trackingAllowed: true,
trackingAsked: false,
daySelected: 0,
sermonSelected: 0,
});
return (
<>
<ConfigContext.Provider value={{ config, setConfig }}>
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'Welcome' }}
/>
</Stack.Navigator>
</NavigationContainer>
</ConfigContext.Provider>
<ExpoStatusBar style="auto" />
</>
);
}
The full code is in the Snack.
I think the issue here is the following:
React useState is asynchron (https://react.dev/reference/react/useState). That means after setting some state variable it takes some time till it is set (depending on the performance on the device).
Fo synchron access you can use Ref (https://react.dev/learn/referencing-values-with-refs) or use side effects with useEffect to listen on changes of the variable
EDIT as the question changed:
Your problem essentially comes down to two points:
useEffect without dependency array:
You call the useEffect(..., );
in your HomeScreen component without a second argument. This means that the effect runs with every render and resets config each time using the stored values. So as soon as you click a button to call setConfig, this triggers a re-render, which in turn triggers the effect and overwrites the values you have just set.
Multiple setConfig calls with stale closure:
In your effect and in the button handlers, you call setConfig({ ...config, trackingAsked: ... })
and immediately afterwards setConfig({ ...config, trackingAllowed: ... })
. However, both calls access the same old config variable (closure), so that the second call “overwrites” the first.
your code can look something like this to make it work:
const { config, setConfig } = useContext(ConfigContext);
useEffect(() => {
(async () => {
let askedBoolean = false;
let allowedBoolean = true;
try {
const asked = await retrieveData('TrackingAsked');
askedBoolean = (asked === 'true');
} catch { /* keeps false */ }
try {
const allowed = await retrieveData('TrackingAllowed');
allowedBoolean = (allowed === 'true');
} catch { /* keeps true */ }
setConfig(prev => ({
...prev,
trackingAsked: askedBoolean,
trackingAllowed: allowedBoolean,
}));
})();
}, []); // only once at Mount
const disableTracking = async () => {
await storeData('TrackingAsked', 'true');
await storeData('TrackingAllowed', 'false');
setConfig(prev => ({
...prev,
trackingAsked: true,
trackingAllowed: false,
}));
};
const enableTracking = async () => {
await storeData('TrackingAsked', 'true');
await storeData('TrackingAllowed', 'true');
setConfig(prev => ({
...prev,
trackingAsked: true,
trackingAllowed: true,
}));
};
const clearAsk = async () => {
await storeData('TrackingAsked', 'false');
setConfig(prev => ({ ...prev, trackingAsked: false }));
};