3

State management in React Native is a well-debated topic, with Redux often being the default choice. However, for developers coming from native mobile development (Kotlin/Swift), Redux can feel cumbersome with its actions, reducers, and dispatching.

As someone experienced in native development, I built my own ViewModel-based state management system in React Native to align better with the MVVM (Model-View-ViewModel) architecture commonly used in Android and iOS. This approach keeps things modular, structured, and easy to scale.

I'll walk you through why I built this system, how it works, and how it compares to Redux.

Why Not Redux?

While Redux is powerful, it comes with some drawbacks:

  • Too much boilerplate: Actions, reducers, dispatching-it's a lot for managing simple state.
  • Overhead for screen-specific state: Redux is global by design, but sometimes we only need state for a single screen.
  • Harder integration with Dependency Injection (DI): Redux doesn't naturally fit DI patterns used in native apps.

For these reasons, I built a ViewModel-based approach that solves these problems while keeping things modular and scalable.

The Core Concept: ViewModels in React Native

In native development, ViewModels handle UI logic and expose state for the View (UI) to consume. I recreated this concept in React Native using React Context and hooks, making it easy to encapsulate both global and screen-specific state.

Here are 3 examples about how I use this ViewModel system:

  1. RootViewModel – Handles app-wide state (e.g., user authentication, theme settings).
  2. TodoListViewModel – A standard ViewModel that lists todo items.
  3. TodoItemViewModel – A ViewModel with data, that takes a todo item ID from the route to manage state for a single item.

This shows how you can have a shared ViewModel, or ViewModels scoped to screens, using data from route or not.

Base definitions

I use the following types to simplify ViewModel creation and usage:

export type ViewModel = React.FC<{ children?: React.ReactNode }>
export type ParamsViewModel<T> = React.FC<{ children?: React.ReactNode } & T>

export const withViewModel: <T extends object>(
    Component: React.FC<T>,
    ViewModel: ViewModel,
) => React.FC<T> = (Component, ViewModel) => (props) => {
    return <ViewModel>
        <Component {...props}/>
    </ViewModel>
}

export const withParamsViewModel: <T extends object>(
    Component: React.FC<T>,
    ViewModel: ParamsViewModel<T>,
) => React.FC<T> = (Component, ViewModel) => (props) => {
    return <ViewModel {...props}>
        <Component {...props}/>
    </ViewModel>
}

Then I can wrap my components with ViewModel like this:

const MyComponent: React.FC = () => {
    const { data } = useSomeViewModel()
    return <></>
}

export default withViewModel(MyComponent, SomeViewModel)

RootViewModel: Managing Global State

My RootViewModel acts as a global store but without the complexity of Redux. It provides essential global data (e.g., user info) and ensures that any ViewModel can access it.

Example:

const RootViewModelContext = createContext({
    user: null as User | null,
    login: async (credentials: Credentials) => {},
})

export const RootViewModel: ViewModel = ({ children }) => {
    const [user, setUser] = useState<User | null>(null)
    const { authRepository } = useDI()
    
    const login = async (credentials: Credentials) => {
        const newUser = await authRepository.login(credentials)
        if (newUser) setUser(newUser)
    }

    return <RootViewModelContext.Provider value={{ user, login }}>
        {children}
    </RootViewModelContext.Provider>
}

export const useRootViewModel = () => useContext(RootViewModelContext)

Now, any component or ViewModel can access global state via useRootViewModel() while keeping setUser private.

TodoListViewModel: Managing a List of Todo Items

This ViewModel is not parameterized and is used to manage a list of todos.

Example:

const TodoListViewModelContext = createContext({
    todos: [] as Todo[],
    fetchTodos: async () => {},
})

export const TodoListViewModel: ViewModel = ({ children }) => {
    const [todos, setTodos] = useState<Todo[]>([])
    const { todoRepository } = useDI()
    
    const fetchTodos = async () => {
        const data = await todoRepository.getTodos()
        setTodos(data)
    }

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

    return <TodoListViewModelContext.Provider value={{ todos, fetchTodos }}>
        {children}
    </TodoListViewModelContext.Provider>
}

export const useTodoListViewModel = () => useContext(TodoListViewModelContext)

TodoItemViewModel: Managing a Single Todo Item

This ViewModel takes a parameter (todo item ID) from navigation and manages the state of a single todo item.

Example:

export const TodoItemViewModel: ParamsViewModel<NativeStackScreenProps<TodoNavigationRoutes, "TodoItemScreen">> = ({
    children, navigation, route
}) => {
    const [todo, setTodo] = useState<Todo | null>(null)
    const { todoRepository } = useDI()

    useEffect(() => {
        todoRepository.getTodo(route.params.todoId).then(setTodo)
    }, [route.params.todoId])

    return <TodoItemViewModelContext.Provider value={{ todo }}>
        {children}
    </TodoItemViewModelContext.Provider>
}

Why This Works Better Than Redux for Screens

  • Encapsulates state per screen instead of storing everything globally.
  • Uses route.params directly, avoiding unnecessary Redux actions.
  • Works seamlessly with Dependency Injection (DI).

Final Comparison: My ViewModel System vs. Redux

Feature ViewModel System Redux
State Location Global (RootViewModel) + per-screen (ParamsViewModel) Global Redux store
Performance Optimized with Context splitting, useMemo, and useCallback Optimized with useSelector
Boilerplate Minimal, simple DI-friendly architecture Requires reducers, actions, and dispatch logic
Navigation Support Uses route.params naturally Requires extra logic to sync navigation state
Best for Modular, self-contained apps Large-scale apps with cross-screen state

Conclusion

If you're coming from native Android/iOS development, this ViewModel approach offers a structured, modular way to manage state in React Native while avoiding Redux’s complexity.

By leveraging RootViewModel for global state, TodoListViewModel for lists, and TodoItemViewModel for single items, you get:

  • A familiar MVVM-like structure.
  • Better performance with minimal re-renders.
  • Less boilerplate than Redux.

What do you think about this approach?

3
  • Looks great! IMO MVVM works very well with React. I'm also writing apps with MVVM but I'm using dependency injection instead of React Context to inject the model into the view model and the view model into the view. This makes the code much more testable. If you're interested I wrote about it here Commented Mar 18 at 7:56
  • 1
    @guy.gc THANK YOU that's exactly what I was looking for (but was unable to find a solution). I have awilix currently for DI, but it's not really good with React (I made a useDI() hook that returns the container to access to my dependencies. Going to study this option because Obsidian looks very promising! Commented Mar 18 at 11:40
  • Hope you find it useful and that it'll fit your needs. Let me know how it works for you I'm interested to hear your thoughts. If you're interested you can reach out on Discord :) Commented Mar 18 at 19:56

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.