Reducers
Reducers are a core concept in Ducky's state management system. They are pure functions that take the current state and an action, and return a new state. Reducers are responsible for determining how the global state should change in response to a given action.
What are Reducers?
Reducers in Ducky are pure functions that dictate how the state of your application should change in response to actions. A reducer takes the current state and an action as inputs and returns a new state as output. This approach ensures that state transitions are predictable and traceable.
Key Characteristics of Reducers
Pure Functions: Reducers are pure functions, meaning they always return the same output for the same input and have no side effects. They do not modify the existing state but instead return a new state object.
Immutability: Reducers enforce immutability by returning a new state object rather than mutating the existing state. This immutability is crucial for ensuring predictable state changes and making state management easier to debug.
Centralized State Management: By centralizing state changes in reducers, you can ensure that all state transformations are handled in a controlled and predictable manner.
Defining Reducers
In Ducky, reducers are defined by extending the SliceReducers<TState>
class, where TState
represents the type of the state slice that the reducer manages. Within a reducer, you map actions to the state changes they should produce.
Example of a Simple Reducer
In this example:
The
CounterReducers
class manages a piece of state representing a simple integer counter.The
On<Increment>
method maps theIncrement
action to a state change that increases the counter by one.The
On<Decrement>
method decreases the counter by one when theDecrement
action is dispatched.The
GetInitialState
method defines the initial state of the counter as0
.
Handling Actions with Reducers
Reducers use the On<TAction>
method to specify how the state should change in response to a specific action. The On
method takes two parameters:
The type of action that the reducer handles.
A function that defines how the state should change in response to the action.
Example of Handling Actions
In this example:
The
SetUserName
action updates theUserName
property of the state.The
SetUserAge
action updates theAge
property of the state.The
state with { ... }
syntax ensures that the state is updated immutably.
State Composition in Ducky
In Ducky, the global state is automatically composed from the individual slice states managed by each reducer. This means that you don't need to manually combine reducers; instead, Ducky handles this composition behind the scenes.
Example: Root State Composition
Each slice reducer manages its own piece of the global state. For example, a UserReducers
might manage the UserState
, while a CounterReducers
manages the CounterState
. Ducky automatically combines these slices into the root state.
When you register reducers in Ducky, they automatically contribute to the overall application state, which Ducky manages as a RootState
object.
Refactoring Reducers with Extracted Methods
As your application grows, your reducers may become more complex. To improve maintainability, you can refactor reducers by extracting logic into separate methods.
Refactored Reducer Example
In this example:
The reducer methods (
ReduceCreateTodo
,ReduceToggleTodo
,ReduceDeleteTodo
) are extracted from the main reducer class, making the class more readable and easier to maintain.Each method is focused on a specific action, further enhancing the clarity and testability of the code.
Initializing State
Each reducer defines an initial state using the GetInitialState
method. This method is called when the store is first created and sets up the default state for the application or the slice managed by the reducer.
Example of Initializing State
The GetInitialState
method provides a default value for the state, ensuring that the application has a well-defined starting point.
Best Practices for Writing Reducers
Keep Reducers Pure: Reducers should not have side effects. Avoid making API calls or dispatching other actions from within a reducer.
Use Immutability: Always return a new state object rather than mutating the existing state.
Decompose State: Break down the state into smaller, manageable slices, each handled by its own reducer.
Handle Actions Explicitly: Each reducer should explicitly handle specific actions. This makes the state transitions clear and predictable.
Use Default States: Ensure that
GetInitialState
provides a meaningful default state for each slice of your application.