| name | app-ui-redux |
| description | Expert for App UI Redux state management - Store, Slices, Reducers, Actions, AsyncThunks, middleware, and Redux DevTools. Use this skill whenever the user wants to manage global state, create a Redux store, define actions and reducers, dispatch events, subscribe to state changes, implement async data fetching with thunks, add middleware for logging or analytics, use the Redux DevTools window, or architect a predictable state management layer in their Unity App UI project. Also trigger when the user mentions ActionCreator, StoreFactory, PartitionedState, or asks about centralized state, immutable records, or state slices. |
| allowed-tools | Bash, Read, Write, Edit, Glob, Grep |
App UI Redux Expert
Expert assistant for implementing Redux state management in Unity using the App UI Redux framework.
Overview
Redux is a predictable state management pattern for Unity applications. It provides a centralized store to manage application state, making it easier to debug, test, and maintain complex state logic. App UI implements Redux using C# with support for slices, async thunks, middleware, and Redux DevTools.
Key Concepts
Why Use Redux?
Use Redux when you need to:
- Manage complex, interconnected state
- Share state across multiple components or systems
- Track state changes for debugging (Redux DevTools)
- Implement predictable state mutations
- Decouple UI from business logic
- Build scalable applications
Core Principles
- Single Source of Truth - All application state is stored in one centralized store
- State is Immutable - State is never modified directly; new state objects are created
- Pure Reducers - Reducers are pure functions with no side effects
- Actions Describe Changes - Actions are dispatched to describe what happened
- Predictability - Same state + same action = same new state
Key Namespace
using Unity.AppUI.Redux;
Store Creation Patterns
Basic Store with StoreFactory
var store = StoreFactory.CreateStore(new[]
{
StoreFactory.CreateSlice(
"sliceName",
new MyState(),
builder => { })
});
Store with Multiple Slices
var store = StoreFactory.CreateStore(new[]
{
StoreFactory.CreateSlice("counter", new CounterState(), counterBuilder => { }),
StoreFactory.CreateSlice("user", new UserState(), userBuilder => { }),
StoreFactory.CreateSlice("todos", new TodosState(), todosBuilder => { })
});
Store with Middleware and Enhancers
var store = StoreFactory.CreateStore(
slices: new[] { },
enhancer: Store.ApplyMiddleware(
LoggerMiddleware(),
ThunkMiddleware()
)
);
Essential Patterns
State Definition using Records
Always use immutable records with init properties:
public record CounterState
{
public int Count { get; init; } = 0;
public string Status { get; init; } = "idle";
}
Action Creators
Without Payload:
public static readonly ActionCreator Increment = "counter/Increment";
With Payload:
public static readonly ActionCreator<int> AddAmount = "counter/AddAmount";
public static readonly ActionCreator<string> SetName = "user/SetName";
Reducers - IMPORTANT: Use AddCase, NOT Add
builder.AddCase(Actions.Increment, (state, action) =>
state with { Count = state.Count + 1 });
builder.AddCase(Actions.AddAmount, (state, action) =>
state with { Count = state.Count + action.payload });
Wrong pattern (do not use):
builder.Add(Actions.Increment, reducer);
AsyncThunk Operations
AsyncThunk creates pending/fulfilled/rejected actions automatically:
public static readonly AsyncThunkCreator<int, string> FetchUserName =
new("user/fetchName", async (userId, api) =>
{
await Task.Delay(1000);
return $"User_{userId}";
});
builder.AddCase(FetchUserName.pending, (state, action) =>
state with { IsLoading = true, Error = null });
builder.AddCase(FetchUserName.fulfilled, (state, action) =>
state with { IsLoading = false, Name = action.payload });
builder.AddCase(FetchUserName.rejected, (state, action) =>
state with { IsLoading = false, Error = "Failed to fetch" });
Slice Configuration
StoreFactory.CreateSlice(
name: "counter",
initialState: new CounterState(),
reducer: builder =>
{
builder.AddCase(Actions.Increment, IncrementReducer);
builder.AddCase(Actions.Decrement, DecrementReducer);
},
extraReducers: builder =>
{
builder.AddCase(MyAsyncThunk.pending, PendingReducer);
builder.AddCase(MyAsyncThunk.fulfilled, FulfilledReducer);
}
);
Store Subscription
var subscription = store.Subscribe<CounterState>("counter", state =>
{
Debug.Log($"Counter: {state.Count}");
});
subscription.Dispose();
Dispatching Actions
Simple Actions:
store.Dispatch(Actions.Increment.Invoke());
store.Dispatch(Actions.AddAmount.Invoke(5));
Async Thunks:
var action = MyAsyncThunk.Invoke(123);
store.Dispatch(action);
await store.DispatchAsyncThunk(action);
State Access
var state = store.GetState<CounterState>("counter");
Debug.Log($"Current count: {state.Count}");
Selectors (Computed State)
Create selector functions for derived state:
public static class Selectors
{
public static bool IsCounterPositive(CounterState state) =>
state.Count > 0;
public static string FormatCount(CounterState state) =>
$"Count: {state.Count}";
}
var isPositive = Selectors.IsCounterPositive(state);
Middleware Configuration
Logger Middleware
public static Middleware<TStore, TStoreState> LoggerMiddleware<TStore, TStoreState>()
where TStore : IStore
{
return (store) => (next) => (action) =>
{
Debug.Log($"[Redux] Dispatching: {action.type}");
var result = next(action);
Debug.Log($"[Redux] Action completed");
return result;
};
}
Custom Middleware
public static Middleware<TStore, TStoreState> CustomMiddleware<TStore, TStoreState>()
where TStore : IStore
{
return (store) => (next) => (action) =>
{
Debug.Log($"Before: {action.type}");
var result = next(action);
Debug.Log($"After: {action.type}");
return result;
};
}
Redux DevTools Integration
The Redux DevTools is available in the Unity Editor:
- Open Window > App UI > Redux DevTools
- Inspect action history and state changes
- Time-travel debug by stepping through actions
- Dispatch custom actions for testing
MVVM Integration
Using Redux with MVVM ViewModels
public interface IStoreService
{
Store Store { get; }
}
public class StoreService : IStoreService
{
public Store Store { get; }
public StoreService()
{
Store = StoreFactory.CreateStore();
}
}
public class MyAppBuilder : UIToolkitAppBuilder<MyApp>
{
protected override void OnConfiguringApp(AppBuilder builder)
{
base.OnConfiguringApp(builder);
builder.services.AddSingleton<IStoreService, StoreService>();
}
}
public class MyViewModel : ObservableObject
{
readonly IStoreService m_StoreService;
public MyViewModel(IStoreService storeService)
{
m_StoreService = storeService;
m_StoreService.Store.Subscribe<MyState>("mySlice", state =>
{
UpdateUI(state);
});
}
}
Common Patterns
Immutable State Updates
Always use the with keyword for immutable updates:
return state with { Count = state.Count + 1 };
state.Count++;
return state;
Error Handling in AsyncThunk
public static readonly AsyncThunkCreator<int, string> FetchData =
new("app/fetchData", async (id, api) =>
{
try
{
var result = await SomeApiCall(id);
return result;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed: {ex.Message}");
}
});
Dispatching from Async Operations
var thunk = new AsyncThunkCreator("operation", async (arg, api) =>
{
api.Dispatch(MyAction.Invoke("starting"));
api.Dispatch(MyOtherAction.Invoke("done"));
return result;
});
Best Practices
- Use Records for State - Immutable by design
- Always Use AddCase - Not Add, for consistency
- Keep Reducers Pure - No side effects, no async operations
- Use AsyncThunk for Async Operations - Not in reducers
- Use Selectors - For derived/computed state
- Subscribe Carefully - Unsubscribe when component/viewmodel is destroyed
- Use Redux DevTools - For debugging complex state flows
- Organize Slices - Group related state together
- Name Actions Descriptively - Use "slice/Action" format
- Test Reducers - Pure functions are easy to unit test
File Organization
YourProject/
├── Redux/
│ ├── Store/
│ │ └── StoreConfiguration.cs
│ ├── Slices/
│ │ ├── CounterSlice.cs
│ │ ├── UserSlice.cs
│ │ └── ...
│ ├── Actions/
│ │ ├── CounterActions.cs
│ │ └── ...
│ ├── Reducers/
│ │ ├── CounterReducers.cs
│ │ └── ...
│ ├── Selectors/
│ │ ├── CounterSelectors.cs
│ │ └── ...
│ └── Middleware/
│ └── CustomMiddleware.cs
Common Issues
Issue: Reducer mutations not reflecting in UI
- Solution: Always use
state with { ... } for immutable updates
Issue: AsyncThunk not updating state
- Solution: Handle pending/fulfilled/rejected cases in extra reducers
Issue: Middleware not being applied
- Solution: Pass enhancer to StoreFactory.CreateStore second parameter
Issue: Subscriptions memory leaks
- Solution: Always dispose subscriptions in OnDestroy/OnDisable
Reference Documentation
Consult reference.md when you need exact API signatures for Store, StoreFactory, ActionCreator, AsyncThunkCreator, or Middleware types, full property/method lists, or advanced patterns like custom enhancers and thunk cancellation. See examples/redux-store.cs for a complete multi-slice store with async operations.