Reputation: 465
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.
While Redux is powerful, it comes with some drawbacks:
For these reasons, I built a ViewModel-based approach that solves these problems while keeping things modular and scalable.
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:
This shows how you can have a shared ViewModel, or ViewModels scoped to screens, using data from route or not.
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)
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.
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.
This ViewModel is not parameterized and is used to manage a list of todos.
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)
This ViewModel takes a parameter (todo item ID) from navigation and manages the state of a single todo item.
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>
}
route.params
directly, avoiding unnecessary Redux actions.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 |
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:
What do you think about this approach?
Upvotes: 2
Views: 31