Install Asset
Install via Godot
To maintain one source of truth, Godot Asset Library is just a mirror of the old asset library so you can download directly on Godot via the integrated asset library browser
Quick Information
A nativescript implementation of Redux for Godot.
Godot Redux Rust
A nativescript implementation of Redux.
Note: This is the source code for the nativescript version of Godot Redux. Although this is in Rust, this is still aimed at people who want to use Redux in gdscript. If you're looking for a pure Rust solution to Redux, I recommend checking out redux-rs. The source code for this can be found at godot-redux-rust-source.
Redux is a way to manage and update your application's state using events called actions. The Redux store serves as a centralized place for data that can be used across your entire application.
Table of Contents
Concepts
The data in Redux is immutable. While this would be great to be able to enforce in gdscript, it is not currently possible and so it is up to you to follow the rules and best practices of how to modify data in your actions as you'll see further down in the Reducers section.
Initial State
The state in redux is stored in an object called the store. While this can be anything, most of the time you'll be using a dictionary of values. Let's take a look at a simple state that has a counter variable:
const state = {
"counter": 0
}
Actions
Actions are the only way to change the state of a Redux application and in gdscript they are represented by an enum. Let's expand the simple counter example from above and create actions to increment and decrement the counter.
enum Action {
INCREMENT,
DECREMENT,
}
Reducers
To actually change the values in the state, we need to create reducers. A reducer is a function that takes the current state and an action and decides how to update the state if necessary. The example below shows how to create a reducer for incrementing and decrementing the counter:
const state = {
"counter": 0,
}
enum Action {
INCREMENT,
DECREMENT,
}
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
A couple things to keep in mind here. First we have to again stress that the state is immutable. This means that you MUST return a new state from your reducer. Second, while you have to return a new state, you can use values from your old state data to create the new state.
Store
So far we've discussed the core components and can put them together into the store. The store must be instanced with the initial state, the current instance, and the name of the reducer function. The current instance and the reducer function name must be provided so that we can keep a reference to it. A full example with the store can look like:
const state = {
"counter": 0,
}
enum Action {
INCREMENT,
DECREMENT,
}
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
Dispatching
To actually update the store you have to use the dispatch
method with the
action you want to run. This will cause the store to run the reducer function
and save the new state value.
store.dispatch(Action.INCREMENT)
This will make the store run the reducer for INCREMENT
and make the counter
go from 0 to 1.
Subscriptions
To listen for changes to the state you can use a subscription. The subscription will be called any time an action is dispatched, and some of the state might have changed. To create a subscriber, you have to pass the instance and the name of the function that should be run when the state is changed like so:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.subscribe(self, 'display_counter')
func display_counter(state):
print(state.counter)
Now whenever the state is changed with dispatch, the display_counter
function will be run and the counter will be printed to the console.
Middleware
Middleware is used to customize the dispatch function. This is done by providing you a point between dispatching an action and it reaching the reducer. Each piece of middleware added will use the action returned by the previous middleware and if nothing is returned then the middleware chain stops being processed.
Below is an example of a middleware function that will take the current action and reverse it:
func reverse_middleware(state, action):
match action {
Action.INCREMENT:
return Action.DECREMENT
Action.DECREMENT:
return Action.INCREMENT
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.add_middleware(self, 'reverse_middleware')
This will actually run the `DECREMENT` action because of our middleware.
store.dispatch(Action.INCREMENT)
Full Example
In this section we'll go through a full example of how you can add Godot Redux to your project and use it in various ways.
If you're in the source repo you'll need to head over to the godot-redux-rust repo that contains the compiled code. Once there, copy the
bin
folder into the root (res://
) directory of your Godot project. If you already have abin
folder in your Godot project then just put the contents of the bin folder into the existing bin folder.Create a new gdscript to create your store in.
In this script, the first thing we need to do is load the
store.gd
script like so:
var Store = load("res://bin/godot_redux/godot_redux.gdns")
- Next, we need to create our initial store. Remember that the store is a Dictionary of values and for our simple counter example it will look something like:
const state = {
"counter": 0
}
- Next we need to define our actions. Actions are defined as an enum like so:
enum Action {
INCREMENT,
DECREMENT,
}
- Now we have to create the reducer function which will dictate what happens when the
INCREMENT
orDECREMENT
action is dispatched.
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
So the reducer function will always take the state and action being dispatched as its arguments. We use a match statement so taht when INCREMENT
is used, the counter will increase by 1 and when DECREMENT
is used, the counter will decrease by 1. Two other things to note here. One, notice that we're returning a new copy of the state in each match arm and two, we can use the previous value of the state to create the new one.
- Let's put this all together and create the
Store
instance. To create a new store instance, we need to pass in our initial state, the class instance that has the reducer function, and the name of the reducer function. Note that this part is different than the gdscript implementation of godot-redux due to limitations with Godot Rust. With the nativescript version, we have to pass the initial state and reducers in theset_state_and_reducer
method like so:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
Since our reducer function is just named 'reducer' and it's on the current class instnace, we can just pass self
and 'reducer` as the last two arguments.
- Now we're ready to try dispatching and see how it affects our state. Let's try dispatching the
INCREMENT
action twice and then theDECREMENT
action once like so:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.dispatch(Action.INCREMENT)
store.dispatch(Action.INCREMENT)
store.dispatch(Action.DECREMENT)
If everything worked correctly, this should put the counter at 1, so let's see:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.dispatch(Action.INCREMENT)
store.dispatch(Action.INCREMENT)
store.dispatch(Action.DECREMENT)
print(store.state()) # { "counter": 1 }
The above is a basic example of how to set up and use the store. We didn't go over all of the available methods though so now we're going to do a couple examples of subscribe
and add_middleware
.
subscribe
- The subscribe
method is used to add a listener to state changes. This means that whenever the state might change, the method that was subscribed will be called and it will be passed the current state as an argument. Below is an example of how you could create a subscriber that would print the counter state whenever it changes:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.subscribe(self, 'counter_printer')
func counter_printer(state):
print(state.counter)
Note that subscribe
is similar to how the store was initialized in that you have to pass the class instance that contains the subscribe function and then the name of the function.
add_middleware
- The add_middleware
method is used to add middleware that can alter the action of a dispatch before it reaches the reducer. The middleware will be passed the current state and the action that was used as arguments. As a simple example, we'll go through creating a middleware that will take the action and return the opposite action:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.add_middleware(self, 'reverse_action')
func reverse_action(state, action):
if (action == Action.INCREMENT):
return Action.DECREMENT
elif (action == Action.DECREMENT):
return Action.INCREMENT
Notice that the add_middleware
is similar to subscribe
in that it takes the class instance that contains the middleware function and then the name of the middleware function as arguments.
We can test to see if this works by dispatching the same actions we did earlier like so:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.add_middleware(self, 'reverse_action')
store.dispatch(Action.INCREMENT)
store.dispatch(Action.INCREMENT)
store.dispatch(Action.DECREMENT)
print(store.state()) # { "counter": -1 }
func reverse_action(state, action):
if (action == Action.INCREMENT):
return Action.DECREMENT
elif (action == Action.DECREMENT):
return Action.INCREMENT
Instead of returning 1 as the value of the counter it should now return -1.
How To Use the Store In Other Scripts
Where you set up the store might not be where you always need to use it. In this case, it might be better to look at autoloading the script where you created your store.
For example let's assume that you want to create a save.gd
script where you'll set up the store. We'll just use the basic counter example from above:
save.gd
var store
const state = {
"counter": 0,
}
enum Action {
INCREMENT,
DECREMENT,
}
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
func _ready():
store = Store.new(state, self, 'reducer')
The only difference here is that we declare store
at the top level so that we can reference it from outside of this script.
Now you can go to Project -> AutoLoad
and select your save.gd
script with a Node name that represents the name of the global variable you'll use to access this script (for this example we'll use Save
).
Lastly, you can create a new script and use your store instance like so:
extends Node
func _ready():
Save.store.dispatch(Save.Action.INCREMENT)
Save.store.dispatch(Save.Action.DECREMENT)
print(Save.store.state()) # { "counter": 0 }
As shown in the example above you can now use your store anywhere by referencing the global Save
variable.
API
new
Creates a new Redux store.
Example:
const state = {
"counter": 0,
}
enum Action {
INCREMENT,
DECREMENT,
}
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
func _ready():
var store = Store.new()
set_state_and_reducer
Sets the initial state and reducer for the store. Normally this is provided on initialization but due to a limitation of Godot Rust, we have to pass these values through this method.
param | type | description |
---|---|---|
state | Dictionary | The initial state of the application. |
reducer_fn_instance | Object | The class instance that contains the reducer function. |
reducer_fn_name | String | The name of the reducer function. |
Example:
const state = {
"counter": 0,
}
enum Action {
INCREMENT,
DECREMENT,
}
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
state
Returns the current state.
Example:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
var state = store.state()
dispatch
Runs the reducer function for the specified action.
param | type | description |
---|---|---|
action | Enum | The action to pass to the reducer. |
Example:
const state = {
"counter": 0,
}
enum Action {
INCREMENT,
DECREMENT,
}
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.dispatch(Action.INCREMENT)
subscribe
Creates a subscriber that gets called whenever the state is changed. The callback function provided will be passed the current state as an argument.
param | type | description |
---|---|---|
callback_fn_instance | Object | The class instance that contains the subscriber callback function. |
callback_fn_name | String | The name of the callback function. |
Example:
const state = {
"counter": 0,
}
enum Action {
INCREMENT,
DECREMENT,
}
func reducer(state, action):
match action:
Action.INCREMENT:
return {
"counter": state.counter + 1,
}
Action.DECREMENT:
return {
"counter": state.counter - 1,
}
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.subscribe(self, 'counter_printer')
func counter_printer(state):
print(state.counter)
add_middleware
Adds a middleware function to intercept dispatches before they reach the reducer. Middleware can be used to change the action to run.
param | type | description |
---|---|---|
middleware_fn_instance | Object | The class instance that contains the middleware function. |
middleware_fn_name | String | The name of the middleware function. |
Example:
func _ready():
var store = Store.new()
store.set_state_and_reducer(state, self, 'reducer')
store.add_middleware(self, 'reverse_action')
store.dispatch(Action.INCREMENT)
store.dispatch(Action.INCREMENT)
store.dispatch(Action.DECREMENT)
print(store.state()) # { "counter": -1 }
func reverse_action(state, action):
if (action == Action.INCREMENT):
return Action.DECREMENT
elif (action == Action.DECREMENT):
return Action.INCREMENT
License
A nativescript implementation of Redux for Godot.
Reviews
Quick Information
A nativescript implementation of Redux for Godot.