React + MobX - not re-rendering update to state - meteor

I've setup a new sample/boilerplate project for testing out using Meteor with React & MobX (using Mantra architecture). The project is at https://github.com/markoshust/mantra-matui-mobx
I'm having an issue where the state change of the State.header.title property is not properly reflecting the updated state change on re-render.
My state is built by pulling in simple objects:
https://github.com/markoshust/mantra-matui-mobx/blob/master/client/modules/core/stores/route.js
Into one master observable object:
https://github.com/markoshust/mantra-matui-mobx/blob/master/client/main.js#L8
I'm listing for route change and calling an action to update state:
https://github.com/markoshust/mantra-matui-mobx/blob/master/client/modules/core/reactions/route.js#L10
This action updates state:
https://github.com/markoshust/mantra-matui-mobx/blob/master/client/modules/core/actions/route.js#L5
The console is logging out proper state change, so the state is being updated properly. However, the component is not being re-rendered with the updated state (this line is console.log'ing old state val):
https://github.com/markoshust/mantra-matui-mobx/blob/master/client/modules/core/containers/Header.js#L6
I'm seeing the 'updating...' message, so the component is re-rendering, but it appears to still be pulling in the old state. I did add observer to all of my react components:
https://github.com/markoshust/mantra-matui-mobx/blob/master/client/modules/core/components/Header.js

I needed to create a custom composer for MobX. I added a listen for autorun to re-compose the component.
https://github.com/markoshust/mantra-matui-mobx/blob/master/client/modules/core/libs/with_mobx.js
import { compose } from 'mantra-core';
import { autorun } from 'mobx';
export default function composeWithMobx(fn, L, E, options) {
const onPropsChange = (props, onData) => {
const reactiveFn = () => fn(props, onData);
autorun(reactiveFn);
return reactiveFn();
};
return compose(onPropsChange, L, E, options);
}

Related

Changing the state management system of existing quasar application from vuex to pinia

Tried this link and created my first store in Quasar using Pinia, I also needed to change the .quasar/app.js manually to add the Pinia store and to make Pinia functional.
import { Quasar } from 'quasar'
import { markRaw } from 'vue'
import RootComponent from 'app/src/App.vue'
import createStore from 'app/src/stores/index'
import createRouter from 'app/src/router/index'
export default async function (createAppFn, quasarUserOptions) {
// Create the app instance.
// Here we inject into it the Quasar UI, the router & possibly the store.
const app = createAppFn(RootComponent)
app.config.devtools = true
app.use(Quasar, quasarUserOptions)
const store = typeof createStore === 'function'
? await createStore({})
: createStore
app.use(store)
const router = markRaw(
typeof createRouter === 'function'
? await createRouter({store})
: createRouter
)
// make router instance available in store
store.use(({ store }) => { store.router = router })
// Expose the app, the router and the store.
// Note that we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}
But the problem is .quasar/app.js is re-written with default contents as soon as quasar dev is executed and again I don't have access to the Pinia stores anymore.
As I said this application was based on vuex formerly.
Make sure you have the index file for pinia.
In "src/stores/index.js"
import { store } from 'quasar/wrappers'
import { createPinia } from 'pinia'
/*
* If not building with SSR mode, you can
* directly export the Store instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Store instance.
*/
export default store((/* { ssrContext } */) => {
const pinia = createPinia()
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})
Try checking quasar info
quasar info
Notice #quasar/app-webpack and vuex.
If you are using #quasar/app, try to move to #quasar/app-webpack by upgrading quasar.
quasar upgrade -i
If you have vuex installed in your quasar info output, try to remove it.
npm uninstall vuex
In your package-lock.json, look for "node_modules/vuex" and delete the key and value.
Then delete your "node_modules" folder and run npm i
After that, run quasar clean.
You may try creating a Pinia store via quasar command to validate it.
quasar new store <store_name>
It should generate a pinia store instead of vuex store.
Problem is older version of #quasar/app-webpack package. It got support for Pinia since v3.4.0. Check release notes here. So basically upgrade this package.
Run quasar upgrade -i and then quasar new store <store_name> [--format ts]
It will create a stores/ directory with pinia.
In my case i didn't need to edit any special files, simply replace the index.js in the stores folder. To get quasar CLI to then use pinia when running quasar new store I had to use quasar clean and just like that I had fully transitioned.
My solution was to remove and reinstall node_modules

Handling namespaced modular approach on PINIA, Vue3+Typescript

normally I was using namespaced vuex. But I am deciding to quit vuex because Pinia has the vue core team support. I think it's better for the future developements. Now I am creating store with a modular approach but couldn't really understand how to handle that part on typescript project.
let's say I have a user interface.
interface User {
email: string,
username: string,
}
export default User;
and in store/modules/state.ts I am calling the Type and creating a user state.
import User from "../../types/User"
export const state = () => {
return {
user: {} as User | null,
};
}
and in store/modules/index.ts I should import the state. And make the namespace: true then export it for the defineStore() for pinia store.
import {state} from "./state"
export default {
namespace: true,
state,
}
in store/index.ts
import {defineStore} from "pinia"
import {data} from "./modules"
export const Store = defineStore(data)
okay above, namespace part I use the vuex way. But what is the right approach for the pinia. Additionally, getters and actions as well. How should export and use them.
According to official Pinia docs:
Vuex has the concept of a single store with multiple modules. These modules can optionally be namespaced and even nested within each other. The easiest way to transition that concept to be used with Pinia is that each module you used previously is now a store.
So now you should think about each vuex module as an separated pinia store. Looking at your example it could look like this. create file in store/modules/index.ts and paste:
import { defineStore } from "pinia";
import state from "store/modules/state.ts"; // Assuming that it's path to user state
export const useUserStore = defineStore('some/vuex/module/name', {
state: state,
getters: {
// your getters here, check the Offical Pinia above
},
actions: {
// your actions and mutations here, also check the offical Pinia Docs
}
})
If you want to split getters, actions and state into multiple files, there is discussion on offical repo issue where I provided example, that is working for me. Here is a link

firebase onSnapshot gets update before create is complete

I have a "post" that listens to changes on its comments in react like so:
// React hook state
const [comments, setComments] = useState([])
// My listener in useEffect
db.collection(`users/${userId}/posts/${postId}/comments`)
.onSnapshot((querySnapshot) => {
let newComments = []
querySnapshot.forEach(function (doc) {
newComments.push({
id: doc.id,
...doc.data()
})
})
setComments(newComments)
})
When the user creates a new comments, I set a loading state and disable the comment section
// React hook
const [isLoading, setLoading] = useState(false)
// Add comment
const addComment = () => {
const comment = {text:"hello"}
setSaving(true)
db.collection(`users/${postUid}/posts/${postId}/comments`).doc()
.set(comment)
.then(()=>{
setSaving(false)
})
}
My problem is (a good problem to have), the subscription onSnapshot gets the new comment before my addComment callback is completed, creating some visual issues:
- Makes the app look buggy when the comment input is still loading but the comment already there
- If there is an error (ex: database permission issue), the comment shows up in the list and then disappears...
Any idea what I can change to not have the onSnapshot update before the create is done?
As explained here in the doc:
Local writes in your app will invoke snapshot listeners immediately.
This is because of an important feature called "latency compensation."
When you perform a write, your listeners will be notified with the new
data before the data is sent to the backend.
Retrieved documents have a metadata.hasPendingWrites property that
indicates whether the document has local changes that haven't been
written to the backend yet.
See also the following remark in the "Listen to multiple documents in a collection" section:
As explained above under Events for local changes, you will receive
events immediately for your local writes. Your listener can use the
metadata.hasPendingWrites field on each document to determine whether
the document has local changes that have not yet been written to the
backend.
So you can use this property to display the change only if it has been written to the back-end, something along the following lines (untested):
db.collection(`users/${userId}/posts/${postId}/comments`)
.onSnapshot((querySnapshot) => {
let newComments = []
querySnapshot.forEach(function (doc) {
if (!doc.metadata.hasPendingWrites) {
newComments.push({
id: doc.id,
...doc.data()
})
}
})
setComments(newComments)
})

Reset state of a Redux store causing incosistent selectors with React Router

I'm experiencing a number of issues trying to reset my state of Redux when changing routes.
On enter in routeA and click browser history to routeB, after firing ##router/LOCATION_CHANGE for routeB it performs the selectors of wrong routeA and causes various undefined state errors.
It should not run the routeA selector, as i'm getting into routeB and I believe this is the problem but I have no idea how to solve it.
In some even happened to be showing duplicate keys in the state, sometimes by the way I'm trying to reset the state wrong.
My scenery
In onEnter of the React Router i execute replaceReducers, see:
My routes onEnter callback:
function onEnter() {
replaceReducers({ book, dragAndDrop })
}
replaceReducers:
function replaceReducers(reducers) {
const appReducers = combineReducers({ ...reducers, ...defaultReducers })
store.replaceReducer((state, action) => {
if (action.type === 'RESET_STORE') {
const { routing, application } = state
state = { routing, application }
}
return appReducers(state, action)
})
store.dispatch({ type: 'RESET_STORE' })
apollo.setStore(store)
}
Steps I followed and what happened the error:
Enter routeA;
Go to routeB;
Click in browser history to routeA;
routeB is called incorrectly;
Error in console:
Uncaught TypeError: Cannot read property 'get' of undefined in routeB-selector.js
getItems: book => book.get('items')
This routeB selector can not be called and something has run it automatically.
Help me, please...

Cant create connected containers or enhancers for griddle

I'm trying to create an Enhancer for griddle v1.6.0.
I am getting this error:
Uncaught Error: Could not find "store" in either the context or props
of "Connect(Row)". Either wrap the root component in a , or
explicitly pass "store" as a prop to "Connect(Row)".
Here is my Enhancer
import { connect } from 'react-redux';
import { selectors } from 'griddle-react';
const { rowDataSelector } = selectors;
export default connect((state, ownProps) => ({
hello: 'world!',
RDZ: rowDataSelector(state, ownProps)
}));
I have a similar problem if I try to create my own Container
EDIT
It appears that the connect is actually connecting to my app's store (which I haven't added a provider for) instead of griddle's internal store. I'm not sure how to access griddle's internal store in an enhancer, or container... Maybe I am missing something here, should I be adding selectors instead?
This a current limitation/bug with Griddle (I think).
I worked around this by passing by data from the app redux to simple component, then that component hands the data to Griddle.
This is not ideal, but works for now.
This discussion maybe of some help.
https://github.com/GriddleGriddle/Griddle/issues/647

Resources