In an application, state is the interface between your data from any kind of backend or local change and the representation of this data with UI elements in the frontend. It could contain every interaction of a user with your application, an object which reflects the currently signed in user or the data from a remote API. State can be a medium to communicate between different components as well.
There are multiple ways you can manage state in your React application, a good approach would be to think of it based on where it is stored in your application:
- Component State: it is inside the component, it's internal to that single component
- Relative State: the state that you pass in through props from parent to child
- Provided State: this state is passed on by providers to the component tree in React using the Context API
- External State: the state is mapped via redux state and allows you to communicate with any component in your app via the redux store.
Redux is a state management tool. It's main purpose is to provide a way for your application to manage its state. The motivation behind the Redux was in response to how so many components were in charge of managing so many different kinds of state. You've got state that is data coming from the API, you've got state that indicates maybe a loading status for a component, you've got state that is managing a local form state. Without some kind of system in place you can run into few problems.
React is great at abstracting the way some challenging implementation details like DOM manipulation, but it essentially leaves the state management decisions and philosophies up to you. So this is where Redux comes in. It's important to know that Redux is just a philosophy for managing your state.
Redux is a highly opinionated library that is best understood by learning about its 3 fundamental guided principles:
1. A single source of truth (global state). Redux solves the complexity of state problem by having you outsource all your data needs to the global state.
2. State is read-only (actions). If you have a component that wants to change the global state in Redux, the only thing it can do is to emit an action that describes what kind of change it wants to make. It hands that action to Redux and lets Redux figure out how to make the change. Then once the state is changed, your component simply reads the new state and makes any UI changes that it needs to make in response.
3. Changes are made with pure functions (reducers). A pure function is one that if it's called with the same parameters over and over again, it will always return the same result. This means that your function can't rely on some outside object or source that may change from one call to the next. The job of a reducer as a pure function is to take the previous version of state and an action and determine a brand new value for the state. Since it's a pure function it must return a new version of state without modifying its old version.
Middleware in React is pretty much like the normal concept of middleware in any other programming language but it sits in between the action creator and the store. Normal flow for any kind of middleware:
- first it captures the previous state
- the action is dispatched to the next middleware function
- all downstream middleware functions in the chain are invoked
- the reducer functions in the store are called with the action payload
Thunks can be used as a middleware in React in order to wait for the payload (the API request) to be resolved. Thunks are a functional programming technique used to delay computation. Instead of performing some work now, you produce a function body or unevaluated expression (the "thunk") which can optionally be used to perform the work later.
So using Thunk middleware in React gives you some advantages:
- thunks enable us to avoid causing side-effects in actions, action creators, or components
- anything impure is wrapped in a thunk for later use in middlewares
- allowing for side-effects to happen only in middlewares, lets your app stay pure
Redux Saga is a middleware that's used in Redux to help us make application side effects (i.e asynchronous things like data fetching) easier to manage, more efficient to execute, easy to test, and better at handling failures.
You can think of a Saga as a separate thread in your application that's responsible for just the side effects and since it's a Redux middleware, it can handle all the normal Redux actions and it can dispatch Redux actions as well.
Sagas are split up into two categories which are Watchers and Workers. Saga's watcher sees every action that is dispatched to the redux store, if it matches the action they are told to handle, they assign it to their worker saga.
- The watcher: watches for dispatched actions and forkes a worker on every action
- The worker: handles the action and terminate
Contrary to redux thunk, you don't end up in callback hell, you can test your asynchronous flows easily and your actions stay pure.
The more complex your application becomes the more complex your state management will become. No matter what rule you use, the state should be as simple and decoupled as possible, but also easy to understand, maintain and test