Nima Zarei
Nima Zarei

Reputation: 1238

How to keep one redux slice from being cleared on logout?

I have a React Native app (using TypeScript and Redux-Toolkit (RTK) Query) in which I want to show an intro slider only for the first app launch and never show it again unless the app data is cleared. I have created a slice for its visibility state and am using redux-persist to keep the data.

Upon logout I'm reseting the store and persisted state by following Dan Abramov's and Lenz Weber-Tronic's recommendations.

// Root reducer (resettable)
const rootReducer = (
  state: ReturnType<typeof appReducer> | undefined,
  action: AnyAction
) => {
  // If the `logout` action is dispatched, clean the `Redux Persist` storage and reset the state
  if (action.type === "auth/logout") {
    storage.removeItem("persist:root");

    return appReducer(undefined, action);
  }

  return appReducer(state, action);
};

I also have separated the persistence config of my intro slice so that I can keep it into storage.

// Root persistence config
const rootPersistConfig = {
  key: "root",
  version: 1,
  storage,
  blacklist: ["intro", api.reducerPath],
};

// Intro slides persistence config
const introPersistConfig = {
  key: "intro",
  version: 1,
  storage,
  blacklist: [api.reducerPath],
};
    
// Top-level reducers
const appReducer = combineReducers({
  [api.reducerPath]: api.reducer,
  auth: authSliceReducer,
  intro: persistReducer(introPersistConfig, introSliceReducer),
});

And this is the intro slice:

interface IntroSliceState {
  /** Whether to show the app intro slides or not */
  showIntro: boolean;
}

const initialState: IntroSliceState = {
  showIntro: true,
};

const introSlice = createSlice({
  name: "intro",
  initialState,
  reducers: {
    hideIntro: (state) => {
      state.showIntro = false;
    },
  },
});

The problem I'm trying to solve now is to find a way to keep the intro slice from being cleared on logout and I know I have to modify this part: return appReducer(undefined, action); but I have no idea how to do it.

Upvotes: 2

Views: 1061

Answers (2)

Robin
Robin

Reputation: 21

Have you tried clearing specific persistors?

storage.removeItem("persist:intro");

EDIT: Sorry just saw the blacklist. You should still try to single it out to rule out other possibilities

But I would probably recommend using a local storage for saving first time launch. There used to be AsyncStorage for react native but here are alternatives:

https://reactnative.directory/?search=storage

If you use expo, you could use:

@react-native-async-storage/async-storage

If you need an example of a first time launch hook + async storage helpers setup send a reply!

EDIT2:

useAppHasLaunched

import { useEffect, useState } from 'react'
import { getAsyncStorageItem, storeAsyncStorageItem } from '../utils/asyncStorageHelper'

export const useAppHasLaunched = () => {
  const HAS_LAUNCHED = 'HAS_LAUNCHED'

  const [hasLaunched, setHasLaunched] = useState<boolean>(false)

  useEffect(() => {
    getData()
  }, [])

  const getData = async () => {
    try {
      const hasLaunched = await getAsyncStorageItem(HAS_LAUNCHED)
      if (hasLaunched) {
        setHasLaunched(true)
      } else {
        await storeAsyncStorageItem(HAS_LAUNCHED, 'true')
      }
    } catch (error) {
      console.log('ERROR_GET_LAUNCH_DATA', error)
    }
  }
  return { hasLaunched, setHasLaunched }
}

asyncStorageHelpers

import AsyncStorage from '@react-native-async-storage/async-storage'

export const getAsyncStorageItem = async (key: string) => {
  try {
    const value = await AsyncStorage.getItem(key)
    if (value) return value
  } catch (error) {
    console.log('Error getting data', error)
  }
  return false
}

export const storeAsyncStorageItem = async (key: string, value: string) => {
  try {
    await AsyncStorage.setItem(key, value)
  } catch (error) {
    console.log('Error storing data', error)
  }
}

Then I use it in my logged out screen:

const { hasLaunched, setHasLaunched } = useAppHasLaunched()

Render onboarding screen, pass in setHasLaunched and use it once your onboarding slider finishes or when you dismiss the slider

{!hasLaunched && <Onboarding setFinishedOnboarding={setHasLaunched} />}

Upvotes: 0

Drew Reese
Drew Reese

Reputation: 203323

Instead of passing undefined state to the reducer so the entire state tree is recomputed from initial state, compute a new state object with only what you want to keep retained.

Example:

// Root reducer (resettable)
const rootReducer = (
  state: ReturnType<typeof appReducer> | undefined,
  action: AnyAction
) => {
  // If the `logout` action is dispatched, clean the `Redux Persist`
  // storage and reset the state
  if (action.type === "auth/logout") {
    storage.removeItem("persist:root"); // *

    // Keep intro state from previous state
    const { intro } = state;
    // Create new state with retain previous state values
    const nextState = {
      intro,
    };

    return appReducer(nextState, action);
  }

  return appReducer(state, action);
};

*Note: storage.removeItem("persist:root"); is one way to delete the persisted state. Another is the persistor.purge method to "clear out the stored state".

Note also that there may actually be no need to manually clear/purge the storage since upon logout and subsequent state update, the new state will be immediately persisted back out to storage.

Upvotes: 0

Related Questions