Debugging study progress and word order in React Native
In this post, I will focus on one of challenges during K-PopRama Slang development: making sure the study screen maintains the correct word order and progress across different situations.
The Study screen has two modes: Daily Picks and Random Study. Users can mark a word as correct or wrong, reset their session, switch modes, and leave the app before returning later.
At first glance, managing the word order seems simple. However, the logic becomes more complicated when the app must also save progress, restore it after restarting, and reset the session when the daily word list changes.
I found that the best way to investigate the behavior was to trace the state changes and useEffect execution step by step instead of looking at each function in isolation.
How the study screen works
The Study screen displays one word at a time from an array called studyWords. The currently displayed word is determined by currentIndex.
For example, imagine that the study list contains five words:
[A, B, C, D, E]
If currentIndex is 0, the app displays A. If the user answers correctly, the correct-answer count increases and the index advances to the next word.
The behavior is different when the user answers incorrectly. In my implementation, the word is appended to the end of the array so that it can appear again later.
For example, if the user answers C incorrectly, the array becomes:
Before: [A, B, C, D, E]
After: [A, B, C, D, E, C]
The current index then advances. This means that the study list is not necessarily a fixed array: its contents can change as the user studies.
The main state variables I needed to manage were:
| State | Purpose |
|---|---|
mode |
Determines whether the user is studying Daily Picks or Random Study. |
studyWords |
Stores the words in their current study order. |
currentIndex |
Identifies the word currently being displayed. |
correctCount |
Counts correctly answered words. |
wrongCount |
Counts incorrectly answered words. |
hasRestored |
Indicates whether the app has finished restoring the saved study state. |
These values are related. Changing the word list can require resetting the index and answer counts, while restoring a saved word list must preserve the existing order rather than shuffle it again.
That relationship was the main reason I needed to review the complete state-management flow.
Saving progress with AsyncStorage
I wanted the study session to survive an app restart. If a user closed the app in the middle of studying and opened it again later on the same day, the app should continue from the saved state.
I used AsyncStorage to store the study progress. The saved data included the date, current index, correct and wrong counts, and the IDs of the words in the current study order.
A simplified version of the saved data looked like this:
const data = { date: getToday(), currentIndex, correctCount, wrongCount, studyWordIds: studyWords.map(word => word.id), }; await AsyncStorage.setItem( STUDY_STORAGE_KEY, JSON.stringify(data) );
I saved the word IDs rather than relying on the complete word objects. When restoring the session, the app could use those IDs to find the corresponding words in the current word pool and reconstruct the saved order.
The date was equally important. The Daily Picks list changes each day, so restoring yesterday's progress into today's list would not make sense.
I therefore used two different persistence rules:
Study mode: Preserve the selected mode across app restarts, even when the date changes.
Study progress: Restore the saved progress only when the saved date matches the current date.
This separation allowed the app to remember the user's preferred mode without carrying yesterday's study progress into a new day.
Why multiple useEffect hooks made the logic difficult
The most challenging part was coordinating several effects that depended on changes to different state variables.
Rather than putting all the logic into one large effect, my implementation used separate effects for restoring saved progress, resetting the session when the word pools changed, initializing the study list, and automatically saving progress.
A. Restore the saved study state
The restore effect loads the saved session from AsyncStorage. It checks the saved date before restoring the index, answer counts, and word order.
If the saved date does not match today's date, the saved progress is discarded and the app starts a new session.
The hasRestored flag is important here. Without it, the effect that initializes the study list could run before the app has finished restoring the saved state.
For example, if the app shuffled the Daily Picks list before loading the saved order, the restored session could lose the sequence the user had been studying.
B. Reset the session when the word pools change
The next effect handles changes to words and randomWords.
When the word pool changes, the session needs to be reset:
useEffect(() => { if (!hasRestored) return; setCurrentIndex(0); setCorrectCount(0); setWrongCount(0); setStudyWords([]); AsyncStorage.removeItem(STUDY_STORAGE_KEY); }, [words, randomWords]);
The hasRestored check prevents this reset logic from running before the initial restoration process has completed.
Resetting studyWords to an empty array is deliberate. It signals that a new study list needs to be prepared.
C. Initialize the study list
A separate effect checks whether the study list is empty. If it is, the app creates the appropriate list for the selected mode.
useEffect(() => { if (!hasRestored) return; if (mode === 'daily' && studyWords.length === 0) { setStudyWords(shuffleArray(words)); } if (mode === 'random' && studyWords.length === 0) { setStudyWords(randomWords); } }, [mode, words, randomWords, hasRestored, studyWords]);
For Daily Picks, the words are shuffled. For Random Study, the app uses the current random word pool.
The empty-array condition is important. If the app has successfully restored a non-empty study list, it should keep that list rather than generate a new order.
D. Automatically save progress
The final effect saves the current state whenever the relevant values change.
useEffect(() => { if (studyWords.length === 0) return; saveStudy( currentIndex, correctCount, wrongCount, studyWords ); }, [ mode, currentIndex, correctCount, wrongCount, studyWords, ]);
I added the empty-list guard because I did not want an intermediate state with no words to be saved as if it were a complete study session.
These effects are easier to understand when viewed separately. However, the real challenge was understanding how they interact when several state values change in quick succession.
Tracing the seven situations
To check the behavior systematically, I listed the situations that could change the study state and traced what should happen in each one.
Case 1: First launch
At the beginning, the study state is initialized with default values:
mode = daily studyWords = [] currentIndex = 0 correctCount = 0 wrongCount = 0 hasRestored = false
The expected sequence is:
The app attempts to load the saved study state.
If there is no valid session to restore, it marks restoration as complete.
The list-initialization effect sees an empty
studyWordsarray and creates the initial study list.The automatic-save effect saves the newly initialized session.
The hasRestored flag prevents the list from being initialized prematurely.
Case 2: Switching study modes
When the user switches between Daily Picks and Random Study, the app must start a new session for the selected mode.
The mode is updated and saved separately. The index and answer counts are reset, the current study list is cleared, and the old progress record is removed.
For Random Study, the app also refreshes the random word pool.
Once the list becomes empty, the initialization effect creates a new list according to the selected mode. The automatic-save effect then saves the new session.
The important requirement is that the old list and its progress must not accidentally carry over into the new mode.
Case 3: Restarting the app on the same day
This case is different from a first launch because a valid session may already exist.
The app restores the saved index, answer counts, and word IDs, then reconstructs the study list in its saved order.
Because studyWords is no longer empty, the initialization effect should not shuffle the Daily Picks list again.
This preserves the user's position and the order of the words, including any words that were moved to the end after incorrect answers.
Case 4: Restarting the app on the next day
The selected mode should remain unchanged, but the previous day's progress should not be restored.
The app compares the saved date with today's date. If the dates differ, it discards the old progress and initializes a new list for the current day.
The expected sequence is:
Load the saved progress and compare dates.
Reject the old progress because the date has changed.
Mark restoration as complete.
Initialize the new study list.
Save the new session.
This case is why I kept mode persistence separate from study-progress persistence.
Case 5: Midnight passes while the app is open
The app must also handle the date changing without being restarted.
In my design, the daily word context updates its word list at midnight. When words or randomWords changes, the reset effect clears the current progress and study list.
The initialization effect then prepares a new list, and the automatic-save effect stores the new session.
This case is particularly important because the app can move through the reset and initialization steps while remaining open on the Study screen.
Case 6: Refreshing the words from the Home screen
The Home screen has a refresh action that changes the Daily Picks word list.
From the Study screen's perspective, the relevant event is the change to the word pool. The same reset logic should clear the old index and answer counts, remove the previous progress record, and initialize a list based on the updated words.
I wanted this behavior to be consistent with the midnight reset. Both events replace the current daily word list, so the Study screen should not continue using a session associated with the old list.
Case 7: Pressing Reset on the Study screen
The Reset button also needs to clear the session, but the behavior depends on the selected mode.
For Daily Picks, the current study list is cleared and then initialized again using the daily words.
For Random Study, the app first refreshes the random word pool and then clears the study list. The initialization effect subsequently uses the refreshed pool.
In both cases, the index and answer counts return to zero. The card is also reset to its front face using a changed key, so the user does not remain on the back of the previous card.
The automatic-save effect then stores the newly initialized session.
The execution flow at a glance
After reviewing the individual cases, I summarized the intended behavior in a simplified flowchart.
This diagram describes the intended high-level flow. The actual React rendering process can involve multiple renders as state updates are applied, so the sequence needs to be checked against the effect dependencies and the conditions inside each effect.
That distinction matters: changing a state variable does not mean that every effect runs, and an effect can run without performing its main action if one of its guard conditions prevents it.
What I learned from tracing the logic
The most useful part of this debugging process was not adding another condition to a single function. It was reviewing the entire lifecycle of the study state.
Several details had to work together:
The restore process had to finish before a new list was initialized.
A valid saved word order had to be preserved rather than shuffled again.
A date change had to reset progress without resetting the selected mode.
Refreshing or resetting the word pool had to clear the old session.
The new list had to be initialized before the updated progress could be saved.
Incorrect answers had to update the word order without losing the user's position in the study session.
I found it helpful to write down the expected state transitions for each situation before changing the code. This made it easier to identify which effect was responsible for a particular transition and which conditions should prevent an effect from acting.
It also reinforced an important lesson about React: when multiple useEffect hooks depend on related state variables, debugging one effect in isolation is often not enough. The behavior must be understood across renders, including the intermediate states between clearing an old list and creating a new one.
For K-PopRama Slang, this work helped me reason more systematically about study progress, word ordering, and persistence. It also gave me a repeatable approach for investigating similar state-management problems in future React Native projects.

Comments
Post a Comment