This article was peer-reviewed by Vildan Softic. Thanks to all the peer reviewers at SitePoint for making SitePoint’s content perfect!
I am the kind of developer who likes to start from scratch and understand how everything works. While I know this brings myself an (unnecessary) workload, it does help me appreciate and understand the mechanisms behind a specific framework, library, or module.
Recently, I have experienced this moment again and started developing a web application using Redux and pure JavaScript. In this article, I want to outline my application structure, examine my early (finally failed) iterations, and then look at the solution I finally chose and what I learned along the way.
You may have heard of the popular React.js and Redux combination that uses the latest front-end technology to build fast and powerful web applications.
React is a component-based open source library created by Facebook for building user interfaces. While React is just a view layer ( is not a complete framework like Angular or Ember), Redux manages the state of the application. It acts as a predictable state container where the entire state is stored in a single object tree and can only be changed by issuing a so-called action. If you don't understand this topic at all, I suggest you read this article.
For the rest of this article, you don't need to be a Redux expert, but at least it will be helpful to have a certain understanding of its concept.
The application in question is a mobile-first Tetris clone with several different views. The actual game logic is done in Redux, while offline functionality is provided by localStorage and custom view processing. The repository can be found on GitHub, although the app is still under active development and I wrote this article during development.
I decided to adopt the folder structure that is common in Redux and React projects. This is a logical structure that works for many different settings. There are many variations of this topic, most projects are slightly different, but the overall structure is the same.
src/scripts/
<code>actions/ ├── game.js ├── score.js └── ... components/ ├── router.js ├── pageControls.js ├── canvas.js └── ... constants/ ├── game.js ├── score.js └── ... reducers/ ├── game.js ├── score.js └── ... store/ ├── configureStore.js ├── connect.js └── index.js utils/ ├── serviceWorker.js ├── localStorage.js ├── dom.js └── ... index.js worker.js</code>
My tags are separated into another directory and end up being rendered by a single index.html file. This structure is similar to scripts/ to maintain a consistent architecture throughout the code base.
src/markup/
<code>layouts/ └── default.html partials/ ├── back-button.html └── meta.html pages/ ├── about.html ├── settings.html └── ... index.html</code>
To access the storage, you need to create and pass it to all instances of the application once. Most frameworks use some kind of dependency injection container, so we as framework users don't have to come up with solutions on their own. But when I use my own solution, how can I make it available for all my components?
My first iteration failed. I don't know why I think this is a good idea, but I put the storage in its own module (scripts/store/index.js) which can then be imported by the rest of the application. I ended up regretting it and quickly dealt with circular dependencies. The problem is that when the component tries to access the storage, the storage is not initialized correctly. I made a chart to demonstrate the dependency flow I'm working on:
Application entry point is initializing all components and then using the storage internally either directly or through helper functions (here referred to as connect). However, since the storage is not created explicitly, but is just a side effect in its own module, the component ends up using the storage before it is created. There is no control over the time when a component or helper function is first called to store. This is very confusing.
Storage module is as follows:
scripts/store/index.js (☓ bad)
import { createStore } from 'redux' import reducers from '../reducers' const store = createStore(reducers) export default store export { getItemList } from './connect'
As mentioned above, the storage is created as a side effect and then exported. Helper functions also need to be stored.
scripts/store/connect.js (☓ bad)
import store from './' export function getItemList () { return store.getState().items.all }
This is exactly the moment when my components end up recursing to each other. Helper functions require storage to run and are exported from the storage initialization file at the same time so that they can access other parts of the application. Do you see how messy this sounds?
It seems obvious now, and it took me a while to understand. I solved this by moving the initialization to my application entry point (scripts/index.js) and passing it to all the required components.
Again, this is very similar to how React actually makes storage accessible (see the source code). There is a reason they work so well together, why not learn its concept?
Application entry point first creates the storage and then passes it to all components. The component can then connect to the storage and schedule operations, subscribe to changes, or get specific data. Let's look at the changes:
scripts/store/configureStore.js
(✓ good)I kept the module, but instead exported a function called configureStore which creates storage elsewhere in the code base.
<code>actions/ ├── game.js ├── score.js └── ... components/ ├── router.js ├── pageControls.js ├── canvas.js └── ... constants/ ├── game.js ├── score.js └── ... reducers/ ├── game.js ├── score.js └── ... store/ ├── configureStore.js ├── connect.js └── index.js utils/ ├── serviceWorker.js ├── localStorage.js ├── dom.js └── ... index.js worker.js</code>
scripts/store/connect.js
(✓ good)connect helper function has basically not changed, but now the storage needs to be passed as a parameter. At first I hesitated to use this solution because I thought
<code>layouts/ └── default.html partials/ ├── back-button.html └── meta.html pages/ ├── about.html ├── settings.html └── ... index.html</code>
. Now I think they are good and advanced enough to make everything easier to read. scripts/index.js
This is the application entry point. The store is created and passed to all components. PageControls adds a global event listener for specific action buttons, and TetrisGame is the actual game component. It looks basically the same before moving the storage here, but does not pass the storage separately to all modules. As mentioned earlier, the component can access the storage via my failed connection method.
import { createStore } from 'redux' import reducers from '../reducers' const store = createStore(reducers) export default store export { getItemList } from './connect'
and container component. Presentational components do nothing but pure DOM processing; they don't know storage. On the other hand, the container component can schedule actions or subscribe to changes. Dan Abramov has written a great article for React components, but this set of methods can also be applied to any other component architecture.
However, there are exceptions for me. Sometimes the components are very small and do only one thing. I don't want to split them into one of the above patterns, so I decided to mix them. If the component grows and gets more logic, I'll separate it.
scripts/components/pageControls.js
The above example is one of the components. It has a list of elements (in this case all elements with data-action attributes) and schedules actions when clicked based on the attribute content. That's all. Other modules may then listen for changes in storage and update themselves accordingly. As mentioned before, if the component also has a DOM update, I will separate it.
import store from './' export function getItemList () { return store.getState().items.all }
Now, let me show you a clear separation of these two component types.
Update DOMI'm actually thinking about doing the same thing, if my application gets bigger and DOM is more tedious I might switch to a virtual DOM, but for now I'm doing classicDOM operation, which works well with Redux.
The basic process is as follows:
Note: I am a fan of the $ symbol prefix for anything related to DOM in JavaScript. As you might guess, it is taken from jQuery's $. Therefore, the pure presentation component file name is prefixed with the dollar sign.
scripts/index.js
<code>actions/ ├── game.js ├── score.js └── ... components/ ├── router.js ├── pageControls.js ├── canvas.js └── ... constants/ ├── game.js ├── score.js └── ... reducers/ ├── game.js ├── score.js └── ... store/ ├── configureStore.js ├── connect.js └── index.js utils/ ├── serviceWorker.js ├── localStorage.js ├── dom.js └── ... index.js worker.js</code>
Nothing fancy here. Import, create, and initialize container component ScoreObserver. What exactly does it do? It updates all the view elements related to scores: high score list and current score information during the game.
scripts/components/scoreObserver/index.js
<code>layouts/ └── default.html partials/ ├── back-button.html └── meta.html pages/ ├── about.html ├── settings.html └── ... index.html</code>
Remember, this is a simple component; other components may have more complex logic and things to deal with. What's going on here? The ScoreObserver component saves internal references to the storage and creates two presentation-level components of the new instance for later use. The init method subscribes to storage updates and updates the $label component every time the storage changes - but only if the game is actually running.
updateScoreBoard method is used elsewhere. It doesn't make sense to update the list every time a change occurs, because the view is inactive anyway. There is also a routing component that updates or deactivates a different component every time the view changes. Its API is roughly as follows:
import { createStore } from 'redux' import reducers from '../reducers' const store = createStore(reducers) export default store export { getItemList } from './connect'
Note: $(and $$) is not a jQuery reference, but a convenient utility shortcut to document.querySelector.
scripts/components/scoreObserver/$board.js
import store from './' export function getItemList () { return store.getState().items.all }
Again, this is a basic example and a basic component. The updateBoard() method takes an array, iterates over it, and inserts the content into the score list.
scripts/components/scoreObserver/$label.js
import { createStore } from 'redux' import reducers from '../reducers' export default function configureStore () { return createStore(reducers) }
This component is almost exactly the same as the ScoreBoard above, but only updates a single element.
Another important point is to implement use case-driven storage. I think it's important to just store content that is essential to the application. At the beginning, I almost stored everything: the current active view, game settings, scores, hover effects, user's breathing mode and so on.
While this may be related to one application, it has nothing to do with another. It might be nice to store the current view and continue in the exact same place when reloaded, but in my case it felt like a bad user experience and more annoying than useful. You don't want to store menus or modal switches, right? Why do users need to return to that specific state? In larger web applications, this may make sense. But in my small mobile game focus game, going back to the settings screen just because I left from there, which is pretty annoying.
I have done my Redux project with and without React, and my main takeaway is that the huge difference in application design is not necessary. Most of the methods used in React can actually fit any other view processing settings. It took me a while to realize this because I thought at first I had to do something different, but I ended up finding it wasn't necessary. However, what is different is how you initialize modules, how you store them, and how well the components understand the overall application state. The concept remains the same, but the implementation and code volume are perfect for your needs.
Redux is a great tool that helps build your application in a more thoughtful way. Using it alone without any view gallery can be very tricky at first, but once you overcome the initial confusion, nothing can stop you. What do you think of my method? Are you using Redux and different views to handle settings alone? I'd love to hear from you and discuss it in the comments.
If you want to learn more about Redux, check out our course "Rewriting and Testing Redux to Solve Design Issues" mini course. In this course, you will build a Redux application that receives tweets organized by topic through a websocket connection. To give you an idea of what’s going to happen, check out the free course below.
Loading the player…FAQ on React-free Redux (FAQ) What is the main difference between using Redux and React and not using React?
Asynchronous operations in Redux are usually handled using middleware such as Redux Thunk or Redux Saga. These middleware allows you to schedule functions (thunks) or more complex asynchronous operations (sagas), rather than ordinary objects. Even without React, you can still use these middleware in Redux storage. You just need to apply middleware when creating storage using Redux's applyMiddleware function.
Yes, Redux DevTools does not depend on React and can be used with any UI layer that uses Redux. You can integrate Redux DevTools into your application by adding it as a middleware when creating Redux storage. This will allow you to check the status and actions of your application in real time, even without React.
Without React and its connect function, you need to manually subscribe to the Redux storage and update the UI components when the state changes. You can subscribe to the store using the store.subscribe method, which takes a listener function that will be called every time the operation is scheduled. In this listener function, you can use store.getState to get the current state of the storage and update the UI components accordingly.
Yes, Redux does not rely on React and can be used with any UI layer. For other libraries and frameworks such as Vue and Angular, bindings are provided that provide similar functionality to React's connect function. These bindings allow you to easily connect UI components to Redux storage and handle updates of components when state changes.
Testing Redux code without React is similar to testing it with React. You can create unit tests for your action creators and reducers using any JavaScript testing framework like Jest or Mocha. For testing asynchronous operations, you can use mock storage to simulate Redux storage.
Side effects in Redux are usually handled using middleware such as Redux Thunk or Redux Saga. These middleware allows you to schedule functions with side effects or more complex asynchronous operations, such as making API calls. Even without React, you can still use these middleware in Redux storage.
Yes, Redux can be used with pure JavaScript. You can create a Redux store, schedule actions to it, and subscribe to changes in the state using only pure JavaScript. However, if there is no library or framework like React to handle updates to the UI, you need to manually update the UI components when the state changes.
The structure of the Redux code does not depend on whether you use React. You can still follow the same best practices for building Redux code, such as separating operations, reducers, and selectors into different files or folders and organizing your state in a normalized and modular way.
Yes, Redux middleware does not rely on React and can be used with any UI layer that uses Redux. Middleware in Redux is used to handle side effects and asynchronous operations, and so on. You can use Redux's apply Middleware to your Redux storage using Redux's applyMiddleware function, whether you use React or not.
The above is the detailed content of Redux without React. For more information, please follow other related articles on the PHP Chinese website!