Large SPA Vuex question - is there a way to abstract common methods that can be inherited or injected into Vuex namespace modules? What I have below works, but feels kludgy as I have to standardize states, etc. I could pass in assoc. array keys to accommodate different state stores, but looking to streamline. Ive look at Vuex plugins and I think there is way to use that to some degree, but again not ideal if I wanted something as simple as getById()
Another approach is to create a Vue IoC factory container, Vue provider, with a Vuex driver, services for for common components, but that is a lot of overhead and I feel would be an overkill, but maybe that is the best approach for a large SPA. Would appreciate some guidance as to where this is the right approach.
vuex-common.js
Collection of common service methods. I could create a common for getters, mutations, actions, etc.
import isArray from 'lodash/isArray'
export function getById () {
return state => {
const find = val => state.data.find(x => x.id === val)
return val => isArray(val) ? val.map(find) : find(val)
}
}
Namespace store example -- store/modules/Users.js
// removed other imports for brevity sake
import { getById } from 'vuex-common'
import forEach from 'lodash/forEach'
import {fetchUsers} from '~/api/apis/users.api';
const initialState = () => ({
data: []
});
const state = initialState();
const getters = {
getById: getById() // vuex-common
// removed other getters for brevity sake
}
const actions = {
async getUsers({commit, getters}) {
try {
const {data} = await fetchUsers();
forEach(data, (u) => {
/* Import getById vuex-common function */
if (!getters.getById(u.id)) {
commit('ADD_USER', u);
}
});
} catch (e) {
console.error("ERROR", e.message, e.name);
}
},
}
// removed mutations and exports for brevity sake
Related
I know Immer library translates all mutable code to immutable one in createSlice() in Redux Toolkit , but I still want to write immutable reducers. Is this code immutable? If not how to correct it? What is immutability in Redux Toolkit and how to implement it?
import { createSlice, PayloadAction } from '#reduxjs/toolkit'
import { Tab } from '../../logic/types'
import { TabsState, updateTabPayload } from '../types'
const initialState: TabsState = {
tabs: []
}
export const tabsSlice = createSlice({
name: 'tabs',
initialState,
reducers: {
loadTabs: (state, tabs: PayloadAction<Tab[]>) => {
state.tabs = [...state.tabs, ...tabs.payload]
},
createTab: (state, tab: PayloadAction<Tab>) => {
state.tabs = [...state.tabs, tab.payload]
},
updateTab: (state, data: PayloadAction<updateTabPayload>) => {
const index = state.tabs.findIndex((tab) => tab.id === data.payload.id)
state.tabs[index] = { ...state.tabs[index], ...data.payload.update }
},
deleteTab: (state, id: PayloadAction<number>) => {
state.tabs = state.tabs.filter((tab) => tab.id !== id.payload)
}
}
})
export const { loadTabs, createTab, updateTab, deleteTab } = tabsSlice.actions
export default tabsSlice.reducer
I have tried to find information about this but still don't get it.
As soon as you write a =, it's not immutable.
An immutable update would for example look like
loadTabs: (state, tabs: PayloadAction<Tab[]>) => {
return {
...state,
tabs: [...state.tabs, ...tabs.payload],
};
}
That said: immutability for the sake of immutability if you don't need it is an absolute fools errand.
Unless you have a good reason to need it, you should opt for writing readable code - and this is a lot harder to read, while bringing no real benefits.
You might try arguing that it is more performant, but that performance boost is absolutely neglectable. It's in the wheelhouse of "could I execute this 10 million times per second or 12 million times?".
Please do yourself a favor and write mutable createSlice case reducers.
More docs here
My goal is to ultimately create add a shopping cart functionality using context API. In order to do so I need to get the products from my database and store it in an array.
Currently, the challenge I'm facing is how to retrieve the data from the axios response and store it in a variable that will be passed on within a const component. Apparently, the issue is that the variable gets passed to the child before the Axios response is complete.
I tried using the await keyword, but got an error regarding not being in an async function. Hence, I tried plugging the async keyword but that didn't work as it yielded errors.
I was able to retrieve data from axios within class components with success, however, I am unable to do so in these const.
Here is my code:
Context.js
import { createContext, useContext, useReducer } from "react";
import React from "react";
import ProductService from "../services/ProductService";
import { cartReducer } from "./Reducers";
const Cart = createContext();
const Context = ({ children }) => {
let products = [];
console.log("part1", products);
ProductService.getAllProducts().then((res) => {
products = res.data; //Also tried setState({ products : res.data})
console.log("response: ", products);
});
const [state, dispatch] = useReducer(cartReducer, {
products: products,
cart: [],
});
return <Cart.Provider value={{ state, dispatch }}>{children}</Cart.Provider>;
};
export const CartState = () => {
return useContext(Cart);
};
export default Context;
ProductService.js
import axios from "axios";
const PRODUCT_BASE_URL = "http://localhost:8080/api/v1/";
class ProductService {
getAllProducts() {
return axios.get(PRODUCT_BASE_URL + "products");
}
getProductsByCategory(category) {
return axios.get(PRODUCT_BASE_URL + "products/" + category);
}
getProductById(id) {
return axios.get(PRODUCT_BASE_URL + "product/" + id);
}
}
export default new ProductService();
Reducers.js
export const cartReducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
HomeComponenet.jsx
import React from "react";
import SlideShowComponent from "./SlideShowComponent";
import HomeCategoriesComponent from "./HomeCategoriesComponent";
import FeaturedProductsComponent from "./FeaturedProductsComponent";
import { CartState } from "../context/Context";
// class HomeComponent extends React.Component
function HomeComponent() {
const { state } = CartState();
console.log("Cart Inside the Home Component: ", state);
return (
<>
<SlideShowComponent />
<div>
<HomeCategoriesComponent />
</div>
<div>
<FeaturedProductsComponent />
</div>
</>
);
}
export default HomeComponent;
First of all, I've created a fixed example here: https://stackblitz.com/edit/react-jqtcia?file=src/Context.js
You are correct, you need to await the result from axios. In React, we use the useEffect hook for things with side effects or that should not be done as part of the render. Renders in react should be non blocking, that is they should not be dependent on things like data fetching.
A simple example of this would be if we needed it in local state. This example renders without the data, then re-renders once the data is available.
const [products, setProducts] = useState([]);
useEffect(async () => {
const { data } = await ProductService.getAllProducts();
setProducts(data);
}, []);
return <div>{products.length > 0 ? `${products.length} products` : 'Loading...'</div>
NOTE: the , []); means that this will fire once, when the first render happens.
This fixes the first part of your problem, getting the result out of the request/axios.
However, the second part and most important part is that you weren't using this value. You were attempting to insert the result as part of the initial state, but this was empty by the time it was created. As you are using reducers (useReducer), this means you need to dispatch an action for each event and handle all the relevant events to data fetching in the reducer. That means, you should need to be able to handle:
Some data is in a loading state (e.g., pagination or first load)
The data failed to load
The data is partially loaded
The data has fully loaded
I've created a minimal happy example (there is no pagination and data fetching always succeeds):
Context.js
useEffect(async () => {
const { data: loadedProducts } = await ProductService.getAllProducts();
console.log('response: ', JSON.stringify(loadedProducts));
dispatch({ type: 'PRODUCTS_LOADED', products: loadedProducts });
console.log(state);
}, []);
Reducers.js
export const cartReducer = (state, action) => {
console.log(action);
switch (action.type) {
case 'PRODUCTS_LOADED':
const newState = { ...state, products: action.products };
console.log(newState);
return newState;
default:
return state;
}
};
In fact, if you'd done this with your original code, it would've worked:
ProductService.getAllProducts().then((res) => {
dispatch({ type: 'PRODUCTS_LOADED', products : res.data});
console.log("response: ", products);
});
However, this has a different bug: It will refetch the data and then dispatch the event each time Context.js is re-rendered.
Since you've asked for more information in your comment, I'll provide it here (comments were not big enough).
I've linked the relevant API documentation for the hooks above, these provide pretty good information. React does a pretty good job of explaining the what, and why of these hooks (and the library itself). Seriously, if you haven't read their documentation, you should do so.
Additional resources:
What, when and how to use useEffect - In short, if you have something to do that isn't rendering such as data fetching, it's a side-effect and should be in a useEffect.
What is a reducer in JavaScript/React/Redux - Reducers are a pattern to make shared state easier to manage and test. The basic idea is that you define a reducer, which takes an initial state and an event/action, and produces a new state. The key idea is that there must be no side-effects, the same action and state will always produce the same result, no matter the date/time, network state, etc, etc. This makes testing and reasoning about things easier, but at the cost of a more complex state management.
However, something important that I ignored in your original question is that you are kind of reinventing the wheel here. There is already a library that will centralise your state and make it available via context, it's the library that originally invented reducers: redux. I'm guessing you have read something like this article about using context instead of redux, however the advantage of redux for you is that there is a litany of documentation about how to use it and solve these problems.
My recommendation for you is to make sure you need/want redux/reducers. It has it's value and I personally love the pattern but if you are just getting started on React, you would be better off just using useState in my opinion.
I have this gtag (analytics plugin) that I can access on my components but never on my store.
I would appreciate any opinions. Thanks
plugins/vue-gtag.js
import Vue from "vue"
import VueGtag from "vue-gtag"
export default ({ app }, inject) => {
Vue.use(VueGtag, {
config: {
id: process.env.ga_stream_id
}
})
}
store/gaUserProperty.js
import Vue from "vue"
import { User } from "~/models/user/User"
export const states = () => ({})
const getterObjects = {}
const mutationObjects = {}
Object.keys(states).forEach(key => {
getterObjects[key] = state => state[key]
mutationObjects[key] = (state, value) => (state[key] = value)
})
export const state = () => states
export const getters = { ...getterObjects }
export const mutations = { ...mutationObjects }
export const actions = {
async sendUserProperties({ dispatch, commit }) {
let res = await this.$UserApi.getUser()
if (!(res instanceof User)) {
} else {
// I can access this on my components and pages but for some reason not here....
console.log(this.$gtag)
}
}
}
To import this properly, I would export the instance (or any of its internals) from main.(ts|js):
const Instance = new Vue({...whatever});
// only export what you need in other parts of the app
export const { $gtag, $store, $t, $http } = Instance;
// or export the entire Instance
export default Instance;
now you can import it in your store:
import Instance from '#/main';
// or:
import { $gtag } from '#/main';
// use Instance.$gtag or $gtag, depending on what you imported.
As other answers mentioned, in current Vuex version the Vue instance is available under this._vm inside the store. I'd refrain from relying on it, though, as it's not part of the exposed Vuex API and not documented anywhere. In other words, Vuex developers do not guarantee it will still be there in future versions.
To be even more specific, Vue's promise is that all v2 code will work in v3. But only if you use the exposed API's.
And the discussion here is not even on whether it will be removed or not (it most likely won't). To me, it's more a matter of principle: if it starts with a _ and it's not documented, it translates into a message from authors: "We're reserving the right to change this at any time, without warning!"
You can access the Vue instance through this._vm in the Vuex store, so you would just need to do:
console.log(this._vm.$gtag)
That should do the trick.
According to nuxtjs the plugins are available in the store actions.
https://nuxtjs.org/docs/directory-structure/plugins
Sometimes you want to make functions or values available across your
app. You can inject those variables into Vue instances (client side),
the context (server side) and even in the Vuex store. It is a
convention to prefix those functions with a $.
I have a mutation can be reused across multiple vuex modules but modifies the state at a module level. How can the mutation be separated out so that it can be dropped into each module's mutations without having to repeat the code?
const state = {
fieldInfo: {}
}
const actions = {
async getOptions({ commit }) {
commit('setOptions', await Vue.axios.options('/'))
}
}
const mutations = {
setOptions(state, value) {
// long mutation happens here
state.fieldInfo = value
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
As you already have your stores namespaced this should work perfectly. All you need to do is move the mutation function to it's own file and then import it in the stores that you need it.
export default function (state, value) {
// long mutation happens here
state.fieldInfo = value
}
Then in your store
import setOptions from './setOptions.js'
const state = {
fieldInfo: {}
}
const actions = {
async getOptions({ commit }) {
commit('setOptions', await Vue.axios.options('/'))
}
}
const mutations = {
setOptions
}
export default {
namespaced: true,
state,
actions,
mutations
}
Someone will probably have a better answer. But in its current state 'fieldInfo' is a shared vuex property. It's like saying window.somVar and expecting to have someVar be a different instance depending on what module is using it. That's not really possible without declaring a new instance of a class, etc. At the most basic level you would need something more like fieldInfo{moduleA: val, moduleB: val}
I have generated a project using Create React App and then added Redux.
The redux state is then split into three parts that each has its own reducer and some middleware defined. The reducers are place in files called part1.js part2.js part3.js there is then a common.js file that imports the reducer and the middleware from part1-2-3.js and adds them to combineReducer and applyMiddeware.
My question is if there is anyway to not having to import everything in one place. What I want is to be able to add the reducer and middeware to comineReducer and applyMiddleware from within part1-2-3.js, the reason is to get rid of an explicit common boilerplate code file in common.js. Is this possible or is the only way to import everything into one place?
UPDATE
I have now great examples on how to solve the combineReducer part, however I still need to do something similar for applyMiddleware. I have found an example from the following repo on how to do something similar with applyMiddleware. However its in TypeScript and I have a hard time translating it into what is the minimal way to get this working within a JS React/Redux application. Would be great with some examples.
UPDATE
So I finally found this minimal library doing what I want.
Yes! I have a reducer registry which is similar (almost identical) to this reducer manager: https://redux.js.org/recipes/code-splitting#using-a-reducer-manager:
const DEFAULT_REDUCER = state => state || null;
export class ReducerRegistry {
constructor() {
this._emitChange = null;
this._reducers = {};
}
getReducers() {
// default reducer so redux doesn't complain
// if no reducers have been registered on startup
if (!Object.keys(this._reducers).length) {
return { __: { reducer: DEFAULT_REDUCER } };
}
return { ...this._reducers };
}
register(name, reducer, options = {}) {
if (this._reducers.name && this._reducers.name !== reducer) {
throw new Error(`${name} has already been registered`);
}
this._reducers = { ...this._reducers, [name]: { reducer, options } };
if (this._emitChange) {
this._emitChange(this.getReducers());
}
}
setChangeListener(listener) {
this._emitChange = listener;
}
}
const reducerRegistry = new ReducerRegistry();
export default reducerRegistry;
Then I have my redux domains organized into folders like reducks-style: https://github.com/alexnm/re-ducks
In the index.js of the reducks domain, I import the reducer registry and register the reducer:
import Domain from './name';
import reducer from './reducer';
import { reducerRegistry } from '...wherever';
reducerRegistry.register(Domain, reducer); // register your reducer
export { ... whatever }
Finally, my store uses the reducer registry like this:
export const store = createStore(
combine(reducerRegistry.getReducers()),
initialState,
composeEnhancers(applyMiddleware(...middlewares))
);
// Replace the reducer whenever a new reducer is registered (or unregistered).!!!!
// THIS IS THE MAGIC SAUCE!
reducerRegistry.setChangeListener(reducers =>
store.replaceReducer(combine(reducers))
);
export default store;
This setup has worked magically for us. Allows us to keep all of our redux logic very isolated from the rest of the application (and from other redux domain logic!), works fantastic for code-splitting. Highly recommend it.
Yes you can. We use it with dynamic import and works well. We use with react hooks.
use store.replaceReducer https://redux.js.org/api/store#replacereducernextreducer
in configureStore (or any file when you call createStore from redux)
const store = createStore(/*...*/)
add
store.injectedReducers = {}; // Reducer registry
and create a new file with injectReducer hook.
const useInjectReducer reducer => {
const store = useStore();
const key = Object.keys(reducer)[0];
if (
Reflect.has(store.injectedReducers, key) &&
store.injectedReducers[key] === reducer[key]
) {
return;
}
store.injectedReducers = {
...store.injectedReducers,
...reducer
};
store.replaceReducer(combineReducers(store.injectedReducers));
}
and you can use it in react App:
export const TodoPage = () => {
useInjectReducer({ [REDUCER_NAME]: TodoReducer });
/*...*/
}
if you use server side rendering you need to be sure redux not cleaning up the states for missing reducers before dynamic import. You can create a dummyReducers to prevent that.
const dummyReducers = Object.keys(initialState).reduce((acc, current) => {
acc[current] = (state = null) => state
return acc;
}, {});
and add this for:
const store = createStore(
combineReducers(dummyReducers),
initialState
)
We use the same pattern to Inject Sagas.