Any way to make sessionStorage reactive in Vue3? - javascript

Lets say I built an app, that fetches data through axios.get from a database every time it loads or is refreshed. This makes the app slow, so I want to only get the data in the initial load of the app and put it into sessionStorage.
Problem is that sessionStorage is not reactive. When I do this:
const state = reactive({
image: sessionStorage.image
})
and want to render:
<div class="home-image" :style="{'background-image': 'url(' + state.image + ')'}"></div>
it only works, if the sessionStorage has been updated in the previous page load. For the initial page load state.image throws a 404 not found.
Is there any way to make sessionStorage reactive? I've read about watchers, set and get but can't quite figure it out.
Solution (new):
Aaaaactually, I just found a way to make sessionStorage behave as if it was reactive during initial load and I don't even need vuex for it:
<script setup>
import {reactive} from 'vue';
const state = reactive({
image: sessionStorage.image || ""
})
axios.get('/home')
.then(res => {
const data = res.data[0]
state.image = sessionStorage.image = 'storage/' + data['image']
})
</script>
This way the reactive function chooses an empty string, if sessionStorage is undefined during the initial load and assigns value from axios.get to sessionStorage. On all consecutive page loads, the reactive function uses sessionStorage, which now has a value assigned to it.
Coding is fun. Sometimes you learn how to replace 100 lines of code with 1 LOC.
Solution (old):
Ok, now my storage works how I want it to. It is global, reactive, persistent and easy to use. I will accept #Elsa's answer because she helped me look into vuex.
I created a store in a seperate store.js file:
import {createStore} from "vuex";
const store = createStore({
state() {
return {
image: sessionStorage.image || ""
}
}
})
export default store
then register it in app.js:
require('./bootstrap');
import {createApp} from 'vue';
import app from "../vue/app";
import store from "./store";
createApp(app)
.use(store)
.mount("#app");
and now I can use store in my components like so:
<template>
<section id="home" class="home">
<div class="image-container">
<div class="home-image" :style="{'background-image': 'url(' + store.state.image + ')'}"></div>
</div>
</section>
</template>
<script setup>
import {useStore} from "vuex";
const store = useStore()
if (!sessionStorage.image) {
axios.get('/home')
.then(res => {
const data = res.data[0]
store.state.image = sessionStorage.image = 'storage/' + data['image']
})
}
</script>
The axios request runs only if sessionStorage.image is undefined, otherwise its value is rendered directly. If the link exists, the image does not get newly loaded in the template first, but rendered instantly.
I might omit state = reactive completely now, since I can use store.state instead globally and even link it to ss/ls. The only thing I have to maintain is the fields inside:
const store = createStore({
state() {
return {
image: sessionStorage.image || ""
}
}
})
because vue throws 404 errors if I don't (but renders the elements anyway because of reactivity).

I had this problem a week ago and I try 3 ways to make a sessionStorage reactive in vue3.
1.vuex
2.create event listener
3.setInterval
I found a temporary solution for my problem with setInterval.
1.vuex
const state = () => {
return {
latInfo: sessionStorage.getItem('lat') || 0
}
}
const getters = {
latInfo: state => state.latInfo
}
3.setInterval
setup() {
setInterval(() => {
if (infoData) {
infoData.lat = sessionStorage.getItem('lat')
}
}, 1000)

Firstly, sessionStorage cannot be reactive.
If my understanding correct, you just want a global variable to be reactive. You can simply achieve it via provide and inject:
const yourGlobalVar = sessionStorage.getItem('image');
app.provide('yourImage', ref(yourGlobalVar))
Where used it:
setup(){
...
const image = inject('yourImage');
...
return {image}
}

Related

How to properly install a Pinia Store?

I'm building a Vue 3 app using the OptionsAPI along with a Pinia Store but I frequently run into an issue stating that I'm trying to access the store before createPinia() is called.
I've been following the documentation to use the Pinia store outside components as well, but maybe I'm not doing something the proper way.
Situation is as follows:
I have a login screen (/login) where I have a Cognito session manager, I click a link, go through Cognito's signup process, and then get redirected to a home route (/), in this route I also have a subroute that shows a Dashboard component where I make an API call.
On the Home component I call the store using useMainStore() and then update the state with information that came on the URL once I got redirected from Cognito, and then I want to use some of the state information in the API calls inside Dashboard.
This is my Home component, which works fine by itself, due to having const store = useMainStore(); inside the mounted() hook which I imagine is always called after the Pinia instance is created.
<template>
<div class="home">
<router-view></router-view>
</div>
</template>
<script>
import {useMainStore} from '../store/index'
export default {
name: 'Home',
components: {
},
mounted() {
const store = useMainStore();
const paramValues = {}
const payload = {
// I construct an object with the properties I need from paramValues
}
store.updateTokens(payload); // I save the values in the store
},
}
</script>
Now this is my Dashboard component:
<script>
import axios from 'axios'
import {useMainStore} from '../store/index'
const store = useMainStore();
export default {
name: "Dashboard",
data() {
return {
user_data: null,
}
},
mounted() {
axios({
url: 'myAPIUrl',
headers: { 'Authorization': `${store.token_type} ${store.access_token}`}
}).then(response => {
this.user_data = response.data;
}).catch(error => {
console.log(error);
})
},
}
</script>
The above component will fail, and throw an error stating that I'm trying to access the store before the instance is created, I can solve this just by moving the store declaration inside the mounted() hook as before, but what if I want to use the store in other ways inside the component and not just in the mounted hook? And also, why is this failing? By this point, since the Home component already had access to the store, shouldn't the Dashboard component, which is inside a child route inside Home have the store instance already created?
This is my main.js file where I call the createPinia() method.
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const pinia = createPinia();
createApp(App).use(router).use(pinia).mount('#app')
And the error I get is:
Uncaught Error: [🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?
My Store file:
import { defineStore } from 'pinia';
export const useMainStore = defineStore('main', {
state: () => ({
access_token: sessionStorage.getItem('access_token') || '',
id_token: sessionStorage.getItem('id_token') || '',
token_type: sessionStorage.getItem('token_type') || '',
isAuthenticated: sessionStorage.getItem('isAuthenticated') || false,
userData: JSON.parse(sessionStorage.getItem('userData')) || undefined
}),
actions: {
updateTokens(payload) {
this.id_token = payload.id_token;
this.access_token = payload.access_token;
this.token_type = payload.token_type
sessionStorage.setItem('id_token', payload.id_token);
sessionStorage.setItem('access_token', payload.access_token);
sessionStorage.setItem('token_type', payload.token_type);
sessionStorage.setItem('isAuthenticated', payload.isAuthenticated);
},
setUserData(payload) {
this.userData = payload;
sessionStorage.setItem('userData', JSON.stringify(payload));
},
resetState() {
this.$reset();
}
},
})
It's possible but not common and not always allowed to use use composition functions outside a component. A function can rely on component instance or a specific order of execution, and current problem can happen when it's not respected.
It's necessary to create Pinia instance before it can be used. const store = useMainStore() is evaluated when Dashboard.vue is imported, which always happen before createPinia().
In case of options API it can be assigned as a part of component instance (Vue 3 only):
data() {
return { store: useMainStore() }
},
Or exposed as global property (Vue 3 only):
const pinia = createPinia();
const app = createApp(App).use(router).use(pinia);
app.config.globalProperties.mainStore = useMainStore();
app.mount('#app');
Since you're using Vue 3, I suggest you to use the new script setup syntax:
<script setup>
import { reactive, onMounted } from 'vue'
import axios from 'axios'
import { useMainStore } from '../store'
const store = useMainStore();
const data = reactive({
user_data: null
})
onMounted (async () => {
try {
const {data: MyResponse} = await axios({
method: "YOUR METHOD",
url: 'myAPIUrl',
headers: { 'Authorization': `${store.token_type} ${store.access_token}`}
})
data.user_data = MyResponse
} catch(error){
console.log(error)
}
})
</script>
Using setup you can define that store variable and use it through your code.
everyone after a lot of research I found the answer to this issue,
you must pass index.ts/js for const like below:
<script lang="ts" setup>
import store from '../stores/index';
import { useCounterStore } from '../stores/counter';
const counterStore = useCounterStore(store());
counterStore.increment();
console.log(counterStore.count);
</script>

How to persist a variable outside React function?

My goal is to avoid invoking a method because of React.useEffect on each re-render. This happens because React thinks the stompClient variable is undefined on each re-render.
This is the current solution I use but this will cause the stompClient to reconnect on each re-render.
App.tsx
import React from "react";
import {stompClient, setStompClient} from "./services/StompClient";
const AuthenticatedContainer = () => {
...
React.useEffect(() => {
if (stompClient?.connected === undefined || stompClient.connected === false) {
// this always happens on each re-render, causing unnecessary re-connection.
setStompClient( the config );
}
});
....
}
./services/StompClient/index.tsx
import {Client, IFrame, StompConfig} from '#stomp/stompjs';
import {environment} from '../../environments/environment';
export let stompClient: Client | null = null;
export const setStompClient = (
onConnect: () => void,
onStompError: (receipt: IFrame) => void,
) => {
const stompConfig: StompConfig = {
brokerURL: `${environment.wsBaseURL}/chat`,
forceBinaryWSFrames: true,
appendMissingNULLonIncoming: true,
onConnect,
onStompError,
};
stompClient = new Client(stompConfig);
};
What I've tried:
I tried to store the StompClient object with redux. It turns out, redux does not like to store non-serializable object. Reference
I tried to create a custom hook, this would create multiple StompClient instead of one. Reference
What I've not tried:
Use React.Context, I am assuming React.Context and Redux is the same, it does not like storing non serializable object. Please feel free to let me know if I am wrong.

How do I modify the default name of react-redux connect function?

I'm using onsen-ui to style a meteor app with React for the frontend. As I understand it, onsen-ui manages navigation by pushing pages to a stack, where each page has a unique identifier.
Here is how my pages are loaded in App.js
loadPage = (page) => {
const currentPage = this.navigator.pages.slice(-1)[0]
if(currentPage.key != page.name){
this.navigator.pushPage(
{
component: page,
props: { key: page.name }
},
{
animation: 'fade',
animationOptions: {duration: 0.3, timing: 'ease-in'}
},
);
}
}
So far everything works fine. But I have now included redux in my project, so I have some components which are connected to the store by react-redux's connect() function.
The problem is that for every component that connect wraps, the name property becomes Connect, so onsen-ui is having to deal with multiple pages with the same name in its stack.
As an example, let's say I have a component defined below
const ComponentName = props => {
return (
<p>Test component</p>
)
}
export default connect()(ComponentName)
ordinarily, ComponentName.name returns ComponentName but once its wrapped with connect, ComponentName.name returns Connect
Is it at all possible to modify the name value for the wrapped component?
Every suggestion is welcome.
Edit: Following Vlatko's lead, this is how I eventually solved the problem.
export const getPageKey = (page) => {
// returns a page key
let key;
if (page.name === 'Connect') {
key = page.displayName
// key = page.WrappedComponent.name
// react-redux connect returns a name Connect
// .displayName returns a name of form Connect(ComponentName)
return key.replace('(', '').replace(')', '')
}
else {
key = page.name
return key
}
}
So for every component I just get the key with getPageKey(ComponentName)
Edit 2. This approach doesn't work in production.
In production mode, I get single letters for page.displayName and those letters are hardly unique, which means I'm back where I started.
I need to find another approach. I'll update whatever I find.
This is happening because you are exporting your component through a higher order function (Connect), which is basically a closure in JavaScript.
The name of the HOC that you are using is Connect, and this is returned.
However, it is possible to get the name of the component passed into the connect HOC.
ComponentName.name // Connect
ComponentName.displayName // Connect(ComponentName)
ComponentName.WrappedComponent.name // ComponentName
I’m not familiar with onsen-ui, and it looks like Vlatko has you covered, but couldn’t you also give the new connected component a name? For example:
const ComponentName = props => {
return (
<p>Test component</p>
)
}
const ConnectedComponentName = connect()(ComponentName)
export default ConnectedComponentName;
Then hopefully you would be able to access the unique ConnectedComponentName

React router + redux navigating back doesn't call componentWillMount

Currently I pre-load data from api in container component's lifecycle method componentWillMount:
componentWillMount() {
const { dept, course } = this.props.routeParams;
this.props.fetchTimetable(dept, course);
}
It is called when user navigates to route /:dept/:course, and it works fine, until you navigate from let's say: /mif/31 to /mif/33 and then press back button. The component is not actually reinitialized, so the lifecycle method is not called, and the data isn't reloaded.
Is there some sort of way to reload data in this case? Should I maybe use another method of preloading data? I see react router emits LOCATION_CHANGE event on any location change, including navigating back, so maybe I can somehow use that?
If it matters, here's is how I implement data loading:
import { getTimetable } from '../api/timetable';
export const REQUEST_TIMETABLE = 'REQUEST_TIMETABLE';
export const RECEIVE_TIMETABLE = 'RECEIVE_TIMETABLE';
const requestTimetable = () => ({ type: REQUEST_TIMETABLE, loading: true });
const receiveTimetable = (timetable) => ({ type: RECEIVE_TIMETABLE, loading: false, timetable });
export function fetchTimetable(departmentId, courseId) {
return dispatch => {
dispatch(requestTimetable());
getTimetable(departmentId, courseId)
.then(timetable => dispatch(receiveTimetable(timetable)))
.catch(console.log);
};
}
You need to use componentWillReceiveProps to check if new props (nextProps) are same as existing props (this.props). Here's relevant code in Redux example: https://github.com/reactjs/redux/blob/e5e608eb87f84d4c6ec22b3b4e59338d234904d5/examples/async/src/containers/App.js#L13-L18
componentWillReceiveProps(nextProps) {
if (nextProps.dept !== this.props.dept || nextProps.course !== this.props.course) {
dispatch(fetchTimetable(nextProps.dept, nextProps.course))
}
}
I might be wrong here, but I believe the function you are looking for is not componentWillMount but componentWillReceiveProps,
assuming you are passing down variables (like :courseId) from redux router to your component, using setState in componentWillReceiveProps should repaint your component.
Otherwise, you can subscribe to changes in your store: http://redux.js.org/docs/api/Store.html
Disclaimer: I probably know less about redux then you.

Redux - Loading initial state asynchronously

I'm trying to work out the cleanest way to load the initial state of my Redux stores when it comes from API calls.
I understand that the typical way of providing the initial state is to generate it server-side on page load, and provide it to Redux createStore() as a simple object. However, I'm writing an app that I'm planning on packaging up in Electron and so this doesn't work.
The best that I've been able to come up with so far is to fire an action immediately after creating the store that will go and request the initial state for the store - either one action that retrieves the entire initial state or a number of actions that each retrieve the initial state for one part of the store. This would then mean that my code looks like:
const store = createStore(reducer, Immutable.Map(), middleware);
store.dispatch(loadStateForA());
store.dispatch(loadStateForB());
store.dispatch(loadStateForC());
Whilst this will work, it seems a bit on the crude side and so I'm wondering if there's some better alternative that I'm missing?
I also encountered the same problem (also building an electron app). A part of my store has application settings which gets persisted on local file system and I needed to load it asynchronously on application's startup.
This is what I come up with. Being a "newbie" with React/Redux, I am very much interested in knowing the thoughts of the community on my approach and how it can be improved.
I created a method which loads the store asynchronously. This method returns a Promise which contains the store object.
export const configureStoreAsync = () => {
return new Promise((resolve) => {
const initialState = initialStoreState;//default initial store state
try {
//do some async stuff here to manipulate initial state...like read from local disk etc.
//This is again wrapped in its own Promises.
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
resolve(store);
});
} catch (error) {
//To do .... log error!
const store = createStore(rootReducer, initialState, applyMiddleware(thunk));
console.log(store.getState());
resolve(store);
}
});
};
Then in my application entry point, here's how I used it:
configureStoreAsync().then(result => {
const store = result;
return ReactDOM.render(
<Provider store={store}>
<App store={store}/>
</Provider>,
document.getElementById('Main'));
});
Like I said, this is my naive attempt at solving this problem and I am sure there must be better ways of handling this problem. I would be very much interested in knowing how this can be improved.
As far as I can tell, you have only two options (logically):
Set the initial state after the store is instantiated
Set the initial state when the store is instantiated
Option 1 must be done using an action:
The only way to change the state is to emit an action, an object
describing what happened.
— One of "Three Principles" in the docs
This is what you've tried, but you think it is crude for some reason.
The alternative is just to call createStore after your asynch request has resolved. One solution has already been posted (by #Gaurav Mantri) using a Promise object, which is a nice approach.
I would recommend against this, since you will likely have multiple modules trying to require or import your store (or store.dispatch, or store.subscribe) before it exists; they would all have to be made to expect Promises. The first method is the most Redux-y.
My app startup workflow:
Loading spinner in index.html
Ajax to check if user is logged in
On ajax end, render the Root component
Hide the loading spinner
I achieved that by:
Creating the store with a custom middleware that listens for the initial ajax end action and calls a callback once
Dispatching the initial ajax action
root.js
const store = createStore(
rootReducer,
applyMiddleware(
...,
actionCallbackOnceMiddleware(INITIAL_AJAX_END, render)
)
)
function render() {
ReactDOM.render(
<Provider store={store}>
<RootComponent/>
</Provider>,
document.getElementById('root')
)
document.getElementById('loading').dispatchEvent(new Event('hide'))
}
store.dispatch(initialAjaxAction());
middleware/actionCallbackOnce.js
export default (actionType, callback) => store => next => {
let called = false;
return action => {
next(action);
if (!called && action.type === actionType) {
called = true;
callback();
}
}
}
index.html
<div id="loading">
<span>Loading</span>
<style type="text/css">...</style>
<script>
(function(loading){
loading.addEventListener('hide', function(){
loading.remove();
});
loading.addEventListener('error', function(){
loading.querySelector('span').textContent = "Error";
});
})(document.getElementById('loading'));
</script>
</div>
<div id="root"></div>
Using extraReducers with createAsyncThunk seems to be the clean way of doing this as explained here
Using async thunks would give you more control. This approach worked for me. In this example the user has a setting for the UI's theme, and this setting will be persisted to the backend. We can't render the UI until we know this setting.
Add an Async Thunk to a Slice: Here we use createAsyncThunk. Async thunks are actions but with the additional ability to (i) perform an API request, (ii) update the state using results from API request. (I'm assuming here you are using redux slices, if you are not then just add this thunk to your main reducer).
// ./store/settings.js
import {
createAsyncThunk,
createReducer,
} from '#reduxjs/toolkit';
import { client } from './api/client';
const initialState = {
theme: 'light', // can be either 'light', 'dark' or 'system'
};
const fetchSettings = createAsyncThunk('settings/fetchSettings', async () => {
const response = await client.fetch('/api/v1/settings');
// `response` is an object returned from server like: { theme: 'dark' }
return response;
});
const settingsReducer = createReducer(initialState, builder => {
builder.addCase(fetchSettings.fulfilled, (state, action) => {
state.theme = action.payload.theme;
});
});
export { fetchSettings };
export default settingsReducer;
Combine Reducers: With slices your state is divided up and so you'll be bringing all your reducers together into one single reducer (some redux boilerplate has bene replaced with // ...):
// ./store/index.js
// ...
// import fooReducer from './store/foo';
// import barReducer from './store/bar';
import settingsReducer from './store/settings';
export const store = configureStore({
reducer: {
// foo: fooReducer,
// bar: barReducer,
settings: settingsReducer,
},
});
// ...
export const { useDispatch, useSelector }
Dispatch Thunk: Dispatching the async thunk will perform the API request and update the store. With async thunks you can use await to wait until this is all done. We won't perform the initial render until this is done.
// ./index.js
import App from './components/App';
import { store } from './store/index';
import { fetchSettings } from './store/settings';
async function main() {
await store.dispatch(fetchSettings());
root.render(
<StrictMode>
<App store={store} />
</StrictMode>,
);
}
main();
Render App: The app will use this updated store and render the theme from the backend.
// ./components/App.js
import { useSelector } from './store/index';
export default function App({ store }) {
// read theme from store
const settings = useSelector(state => state.settings);
const settingsTheme = settings.theme;
return (
<Provider store={store}>
<div>Your app goes here. The theme is ${settingsTheme}</div>
</Provider>
);
}

Categories

Resources