
We previously looked at how you could maintain state withDojo containers using a context for your application. To recap, containers are basically higher order components that you can use to add state management to widgets.
Using a context with containers makes the process fairly simple, however if you want to wire up multiple containers and routes with shared state, you might want to start looking at usingDojo Stores.
Dojo Stores work by usingProcesses to executeCommands with varyingOperations against your state. You don't need to define your operations, Dojo provides them for you. These operations are based on theJSON Patch format. They currently support add, remove, replace, and test. If you're not familiar with these operations or they look intimidating, don't worry, they are much simpler than they may seem at first.
In order to try and grasp how everything works, let's create a small application that explores theStar Wars API.
API Explorer Widget
First thing we'll do is create a widget that can explore the API. I'll use aListbox to display the endpoints of the API and aTextArea to display the raw JSON results of the selected endpoint.
// src/widgets/APIExplorer.tsximport{tsx}from"@dojo/framework/widget-core/tsx";import{WidgetBase}from"@dojo/framework/widget-core/WidgetBase";import{watch}from"@dojo/framework/widget-core/decorators/watch";importListboxfrom"@dojo/widgets/listbox";importTextAreafrom"@dojo/widgets/text-area";importthemefrom"@dojo/themes/dojo";import*ascssfrom"./styles/APIExplorer.m.css";import{ExplorerProperties,Item}from"../interfaces";exportclassAPIExplorerextendsWidgetBase<ExplorerProperties>{@watch()index=0;@watch()label="";onAttach(){this.properties.fetchAllResults();}protectedrender(){return(<divclasses={css.root}><Listboxtheme={theme}key="listbox1"activeIndex={this.index}widgetId="listbox1"optionData={this.properties.items}getOptionLabel={(option:Item)=>option.label}getOptionSelected={(option:Item)=>option.label===this.label}onActiveIndexChange={(index:number)=>{this.index=index;}}onOptionSelect={(option:Item,index:number)=>{this.label=option.label;this.properties.fetchData(option);}}/><TextArearows={15}theme={theme}value={this.properties.result}/></div>);}}// src/interfaces.tsexportinterfaceExplorerProperties{items:Item[];result:string;fetchAllResults:()=>void;fetchData:(item:Item)=>void;}
This widget has some local state to manage the selected value of the Listbox, but other than that, it relies on the properties passed to it in order to display anything useful. I'll be using a container to pass those properties to this widget. But first, how would I make my queries and actually update my application state in the first place?
Processes
Processes are used to execute commands against your application store. It's in the processes that you are going to be doing most of the heavy lifting for your application. This is where you can do queries, transformations, filtering, validation, and so on. There are factory functions in Dojo to help you create your commands.
// src/processes/starwarsProcesses.tsimport{createProcess,createCommandFactory}from"@dojo/framework/stores/process";import{replace}from"@dojo/framework/stores/state/operations";import{State,Item}from"../interfaces";// commandFactory typed to my application stateconstcommandFactory=createCommandFactory<State>();
In this application, I'm going to use thereplace operation to make my state updates. If I were adding items to an array in my commands, I could do some interesting things using theat helper andadd operation to insert new items into an arrayat specific indices.
Let me create a command that will fetch all the available endpoints of the Star Wars API.
// src/processes/starwarsProcesses.tsconstfetchItems=commandFactory<Item>(async({path})=>{constresponse=awaitfetch("https://swapi.co/api/");constjson=awaitresponse.json();constitems:Item[]=Object.keys(json).map(key=>{return{label:key,value:json[key]};});return[replace(path("items"),items)];});
I use my command factory to create a function that will make my query and then returns an array of operations. Why any array? Because I may want a single command do multiple state updates. In this case I use[replace(path("items"), items)]. This may look odd at first, but the path method is a way to designate a property path in my state. If I want to use the value from my state I could use aget helper and writeget(path("items")), which would return the items array.
I was unsure how I felt about this method of accessing state at first, but it lends itself very well to a functional and reactive method of interacting with state that has really grown on me. This behavior is very much like working withlenses.
Now, I'll create a command to fetch the results of one of the selected endpoints.
// src/processes/starwarsProcesses.tsconstfetchResult=commandFactory<Item>(async({path,payload})=>{constresponse=awaitfetch(payload.value);constresult=awaitresponse.json();return[replace(path("result"),JSON.stringify(result,undefined,2))];});
Very similar to the previous command I am going to fetch the results of an endpoint, but that endpoint URL is coming from apayload object that was passed to my command. I've typed this payload to have avalue property, but it will be any argument that has been passed to the process that will execute this command. We'll see how that is done in a moment, but first we need to create a couple of processes that will be used to execute these commands.
// src/processes/starwarsProcesses.tsexportconstfetchItemsProcess=createProcess("fetch-items",[fetchItems]);exportconstfetchResultProcess=createProcess("fetch-result",[fetchResult]);
We create two processes to do two distinct operations on my state. What I find interesting here is that I can have a single process execute multiple commands that could in turn perform multiple operations against my state. I haven't done this yet, but I find the fact thatI could to be awesome!
Container
Let's wrap our widget in a container that will use our processes to wire up properties and methods.
// src/containers/APIExplorerContainer.tsimport{Store}from"@dojo/framework/stores/Store";import{StoreContainer}from"@dojo/framework/stores/StoreInjector";import{APIExplorer}from"../widgets/APIExplorer";import{State}from"../interfaces";import{fetchItemsProcess,fetchResultProcess}from"../processes/starwarsProcesses";functiongetProperties(store:Store<State>):any{const{get,path}=store;return{items:get(path("items")),result:get(path("result")),fetchAllResults:fetchItemsProcess(store),fetchData:fetchResultProcess(store)};}// Use a StoreContainerexportconstAPIExplorerContainer=StoreContainer(APIExplorer,"state",{getProperties});
We are using a specific container called aStoreContainer that will inject our store in to ourgetProperties method. This method is how you can pass properties to your wrapped widget from the container.
You can see here that the store has helper methods,get andpath, that I mentioned before to access the values of properties on the store. I can now pass processes that will execute commands against the store as methods that my widget is expecting as part of its properties.
Once this is done, I need to register my store with my application and inject it.
// src/main.tsxconststore=newStore();constregistry=registerStoreInjector(store);classAppextendsWidgetBase{protectedrender(){return<APIExplorerContainer/>;}}constr=renderer(()=><App/>);r.mount({registry});
I use a helper calledregisterStoreInjector and then inject that store into a named state that I used in my container.
What you end up with is an application like thison codesandbox.
Summary
There is a lot happening here, but what it boils down to is the following steps.
- Create processes to execute commands against a Store
- Wrap widget in a container to pass processes and store properties
- Inject store into application
Any subsequent updates the store will be passed down to the widget. There is even more you could do with your store. I could have wrapped my widget in aStoreProvider and passed store properties down, and manuallysubscribe to store changes. Subscribing to store changes could be a very useful tool inside your applications for some granular control of managing widget visibility for example.
Processes even provide the ability to addmiddleware so you can add logging and authentication, or maybe payload validation without having to pollute your processes with extra code and possibly extra sources of errors.
At the end of the day, I have had a fun time learning how to use processes and stores in my applications. They provide a lot of flexibility in managing state and I can already see how useful they would be in building out an application as it grows!
Be sure to subscribe to the newsletter and stay up to date with the latest content!
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse