How to use pinia getter in setup ? (with a wrapped defineStore and async fetching) - asynchronous

I don't succeed to use a Pinia getter properly in the setup part.
It's working when I use the getter directly in the part, but not in the part.
Here is my (simplified) pinia store :
export const useCharactersStore = () => {
const charactersStoreBuilder = defineStore("characters", {
state: (): State => ({
characters: {},
}),
getters: {
getCharacter(state) {
return (characterId: string) => state.characters[characterId];
},
},
actions: {
async fetchCharacters() {
this.characters = {
1: "Character 1",
2: "Character 2",
};
},
},
});
const charactersStore = charactersStoreBuilder();
charactersStore.fetchCharacters(); // async fetching
return charactersStore;
And here is my usage :
<script setup>
import { useCharactersStore } from "#/stores/characters";
import { storeToRefs } from "pinia";
const characterId = 1;
const characterStore = useCharactersStore();
// Various alternatives :
// Don't work
const { getCharacter } = storeToRefs(characterStore);
const character = getCharacter.value(characterId);
// Don't work
const character = characterStore.getCharacter(characterId);
// Don't work
const character = characterStore.getCharacter(characterId);
// Work
const getCharacter = characterStore.getCharacter;
</setup>
<template>
<pre>
<!-- Don't work -->
{{ character }}
<!-- Work -->
{{ getCharacter(characterId) }}
</pre>
</template>
I've also tried with character as a ref, with its value updated with a watch, and various things. I'm absolutely lost :/

Your question is ambiguous. I do not know what you mean by.
It's working when I use the getter directly in the part, but not in the part.
However, I made a slight modification to your code. For me, this works.
// CharactersStore.ts
import { defineStore } from 'pinia'
export const useCharactersStore = () => {
const charactersStoreBuilder = defineStore('CharactersStore', {
state: () => {
return {
characters: {} as Record<number | string, string | number>,
}
},
actions: {
// async is irrelevant here. You can use setTimeout to simulate async here.
async fetchCharacters() {
this.characters = {
1: 'Character 1',
2: 'Character 2',
}
},
},
getters: {
getCharacterByID: (state) => (characterId: string | number) =>
state.characters[characterId],
},
})
const charactersStore = charactersStoreBuilder()
charactersStore.fetchCharacters() // async fetching
return charactersStore
}
The Test page I used.
<script setup lang="ts">
import { storeToRefs } from 'pinia'
const c = useCharactersStore()
const characterId = 1 // even though it works, It is not reactive.
const character = c.getCharacterByID(characterId)
const { getCharacterByID: getCharacterWithStoreRef } = storeToRefs(c)
</script>
<template>
<div>
<h1>Test page</h1>
<div>
<pre>
Get character by Id : {{ c.getCharacterByID(characterId) }}
character {{ character }}
getCharacterWithStoreRef {{ getCharacterWithStoreRef(characterId) }}
</pre>
</div>
</div>
</template>
Result
Test the solution provided
Feel free to let me know if it works for you. Happy coding
Additional information
Read more about Passing arguments to getters in Pinia

Related

Storybook Vue3 - Work with v-model in stories

I have a question regarding Storybook and Vue components with v-models. When writing a story for let's say an input component with a v-model i want a control reflecting the value of this v-model. Setting the modelValue from the control is no problem, but when using the component itself the control value stays the same. I am searching the web for a while now but i can't seem to find a solution for this.
A small example:
// InputComponent.vue
<template>
<input
type="text"
:value="modelValue"
#input="updateValue"
:class="`form-control${readonly ? '-plaintext' : ''}`"
:readonly="readonly"
/>
</template>
<script lang="ts">
export default {
name: "GcInputText"
}
</script>
<script lang="ts" setup>
defineProps({
modelValue: {
type: String,
default: null
},
readonly: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue']);
const updateValue = (event: Event) => {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
}
</script>
In Storybook:
Does anyone have a solution to make this working?
Thanks in advance!
In my case, I have a custom select input that uses a modelValue prop.
I tried this and worked for me:
at my-component.stories.js:
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
export default {
title: 'Core/MyComponent',
component: MyComponent,
argTypes: { }
}
const Template = (args) => ({
components: { MyComponent },
setup() {
let model = ref('Javascript')
const updateModel = (event) => model.value = event
return { args, model, updateModel }
},
template: '<my-component v-bind="args" :modelValue="model" #update:modelValue="updateModel" />'
})
export const Default = Template.bind({})
Default.args = {
options: [
'Javascript',
'PHP',
'Java'
]
}

Component is being rendered twice in SSG page in Next.js

I'm currently trying using utterances, which is a github-based open source for comments.
I'm using utterances in my SSG page. Therefore, I'm using client side rendering for getting the utterances component.
Here is the code.
// blog/[id].tsx
/* eslint-disable react/no-danger */
import axios from 'axios';
import { dateFormat } from '_Utils/Helper';
import MarkdownRenderer from '_Components/MarkdownRenderer';
import Comment from '_Components/Comment';
import styles from './blog.module.scss';
const Article = ({ article }: any) => {
return (
<div className={styles.container}>
<div className={styles.header}>
<p className={styles.tag}>{article.data.attributes.tag.data.attributes.tag}</p>
<h1>{article.data.attributes.title}</h1>
<p className={styles.publishedDate}>Published at {dateFormat(article.data.attributes.publishedAt)}</p>
</div>
<main
>
<MarkdownRenderer markdown={article.data.attributes.content} />
<Comment />
</main>
</div>
);
};
export async function getStaticPaths() {
const articlePaths: any = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/api/articles/?populate[0]=*`);
const paths = articlePaths.data.data.map((path: any) => ({
params: { id: `${path.id}` },
}));
return { paths, fallback: false };
}
export async function getStaticProps(ctx: any) {
const { params } = ctx;
const { id } = params;
const article = await axios.get(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/articles/${id}?populate[1]=tag&populate[0]=thumbnail`
);
return {
props: { article: article.data },
};
}
export default Article;
// Comment
const Comment = () => {
return (
<section
style={{ height: '350px', width: '100%' }}
ref={(elem) => {
if (!elem) {
return;
}
const scriptElem = document.createElement('script');
scriptElem.src = 'https://utteranc.es/client.js';
scriptElem.async = true;
scriptElem.setAttribute('repo', 'usernamechiho/Cobb-dev-blog');
scriptElem.setAttribute('issue-term', 'title');
scriptElem.setAttribute('theme', 'github-light');
scriptElem.setAttribute('label', 'comment');
scriptElem.crossOrigin = 'anonymous';
elem.appendChild(scriptElem);
}}
/>
);
};
export default Comment;
and the result
I was wondering why it happens and tried dynamic import with ssr: false.
However, there was nothing but the same.
Is there anything I can look for to get through this?

How to use Firestore in Nuxt3 with SSR?

I am using Nuxt RC8 combined with Firestore.
My goal is to make the firestore request SSR and then combine it with Firestore's onSnapshot to get realtime updates after hydration is done.
I have created this composable useAssets:
import { computed, ref } from 'vue';
import { Asset, RandomAPI, RandomDatabase } from '#random/api';
/**
* Asset basic composable
* #param dbClient Database client
* #param options Extra options, like live data binding
*/
export function useAssets(dbClient: RandomDatabase) {
const assets = ref([]);
const unsubscribe = ref(null);
const searchQuery = ref('');
const randomAPI = RandomAPI.getInstance();
async function fetchAssets(options?: { live: boolean }): Promise<void> {
if (options?.live) {
try {
const query = randomAPI.fetchAssetsLive(dbClient, (_assets) => {
assets.value = _assets as Asset<any>[];
});
unsubscribe.value = query;
} catch (error) {
throw Error(`Error reading assets: ${error}`);
}
} else {
const query = await randomAPI.fetchAssetsStatic(dbClient);
assets.value = query;
}
}
const filteredAssets = computed(() => {
return searchQuery.value
? assets.value.filter((asset) =>
asset.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
: assets.value;
});
function reverseAssets(): void {
const newArray = [...assets.value];
assets.value = newArray.reverse();
}
return {
assets,
fetchAssets,
filteredAssets,
searchQuery,
reverseAssets,
unsubscribe,
};
}
The randomAPI.fetchAssetsLive comes from the firestore queries file:
export function fetchAssetsLive({
db,
callback,
options,
}: {
db: Firestore;
callback: (
assets: Asset<Timestamp>[] | QueryDocumentSnapshot<Asset<Timestamp>>[]
) => void;
options?: { fullDocs: boolean };
}): Unsubscribe {
const assetCollection = collection(db, 'assets') as CollectionReference<
Asset<Timestamp>
>;
if (options?.fullDocs) {
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs)
);
}
// Return unsubscribe
return onSnapshot(assetCollection, (querySnapshot) =>
callback(querySnapshot.docs.map((doc) => doc.data()))
);
}
And then the component:
<template>
<div>
<h1>Welcome to Random!</h1>
<Button #click="reverseAssets">Reverse order</Button>
<ClientOnly>
<!-- <Input name="search" label="Search for an asset" v-model="searchQuery" /> -->
</ClientOnly>
<ul>
<li class="list-item" v-for="asset in assets" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { Button, Input } from '#random/ui';
import { useNuxtApp, useAsyncData } from '#app';
const { $randomFirebase, $firestore, $getDocs, $collection } = useNuxtApp();
const { fetchAssets, filteredAssets, searchQuery, reverseAssets, assets } =
useAssets($randomFirebase);
// const a = process.client ? filteredAssets : assets;
onMounted(() => {
// console.log(searchQuery.value);
// fetchAssets({ live: true });
});
watch(
assets,
(val) => {
console.log('watcher: ', val);
},
{ deep: true, immediate: true }
);
// TODO: make SSR work
await useAsyncData(async () => {
await fetchAssets();
});
</script>
Why is it only loading via SSR and then assets.value goes []? Refreshing the page retrieves renders the items correctly but then once hydration comes in, it's gone.
Querying both, in onMounted and useAsyncData, makes it send correctly via SSR the values, makes it work client-side too but there is still a hydration missmatch, even being the values the same. And visually you only see the ones from the client-side request, not the SSR.
Is there a better approach? What am I not understanding?
I don't want to use firebase-admin as the SSR query maker because I want to use roles in the future (together with Firebase Auth via sessions).
I solved the hydration issue in two ways:
By displaying in the template only specific information, since JS objects are not ordered by default so there could be different order between the SSR query and the CS query.
By ordering by a field name in the query.
By making sure that the serverData is displayed until first load of the onsnapshot is there, so theres is not a mismatch this way: [data] -> [] -> [data]. For now I control it in the template in a very cheap way but it was for testing purposes:
<li class="list-item" v-for="asset in (isServer || (!isServer && !assets.length) ? serverData : assets)" :key="asset.name">
Asset Name: {{ asset.name }} Type: {{ asset.type }}
</li>
By using /server/api/assets.ts file with this:
import { getDocs, collection, query, orderBy, CollectionReference, Timestamp, Query } from 'firebase/firestore';
import { Asset } from '#random/api/dist';
import { firestore } from '../utils/firebase';
export default defineEventHandler(async (event) => {
const assetCollection = collection(firestore, 'assets');
let fullQuery: CollectionReference<Asset<Timestamp>> | Query<Asset<Timestamp>>;
try {
// #ts-ignore
fullQuery = query(assetCollection, orderBy('name'));
} catch (e) {
console.error(e)
// #ts-ignore
fullQuery = assetCollection;
}
const ref = await getDocs(fullQuery);
return ref.docs.map((doc) => doc.data());
});
And then in the component, executing:
const { data: assets } = useFetch('/api/assets');
onMounted(async () => {
fetchAssets({ live: true });
});
Still, if I try via useAsyncData it does not work correctly.

How to test computed value inside setup function in Vue.js 3 with vue-test-utils & Jest

I am getting "TypeError: Cannot add property myData, object is not extensible" on setData
Hello.vue
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button"> Edit </button>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, reactive} from "vue"
export default defineComponent({
setup() {
const myObject = {myName:"", myNumber:""}
let myData = reactive({myObject})
const isEditable = computed(() => {
return myData.myObject.myName.startsWith('DNU') ? false : true
})
return {
isEditable
}
}
})
</script>
Hello.spec.ts
import { shallowMount } from '#vue/test-utils'
import Hello from '#/components/Hello.vue'
import { reactive } from 'vue'
describe('Hello.vue Test', () => {
it('is isEditable returns FALSE if NAME starts with DNU', async () => {
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
expect(wrapper.vm.isEditable).toBe(false)
})
})
I also tried to see if that DIV is visible by:
expect(wrapper.find('#myEditDiv').exists()).toBe(false)
still same error. I might be completely off the path, so any help would be appreciated.
Update
This is possible several different ways. There's two issues that need to be addressed.
The variable has to be made available. You can use vue's expose function in setup (but getting the value is really messy: wrapper.__app._container._vnode.component.subTree.component.exposed😱) or just include it in the return object (accessible through wrapper.vm).
change how you mutate the data in the test.
your test has
const myObject = {myName:"DNU Bad Name", myNumber:"12345"}
let myData = reactive({myObject})
const wrapper = shallowMount(Hello)
await wrapper.setData({'myData' : myData})
even if setData was able to override the internal, it would not work.
the problem is that the setup function has this
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
where editable is using a computed generated from that instance of myData. If you override myData with a separate reactive, the computed will still continue to use the old one. You need to replace the contents of the reactive and not the reactive itself
To update the entire content of the reactive, you can use:
Object.assign(myReactive, myNewData)
you can make that a method in your component, or just run that from the test. If you update any value within the reactive (like myData.myObject) you can skip the Object.asign
Here are several versions of how you can test it.
Component:
<template>
<div v-if="isEditable" id="myEditDiv">
<button type="button">Edit</button>
</div>
</template>
<script>
import { computed, defineComponent, reactive } from "vue";
export default defineComponent({
setup(_, { expose }) {
const myObject = { myName: "", myNumber: "" };
let myData = reactive({ myObject });
const isEditable = computed(() => {
return myData.myObject.myName.startsWith("DNU") ? false : true;
});
const updateMyData = (data) => Object.assign(myData, data);
expose({ updateMyData });
return {
isEditable,
updateMyData,
myData
};
},
});
</script>
the test
import { shallowMount } from "#vue/test-utils";
import MyComponent from "#/components/MyComponent.vue";
const data = { myObject: { myName: "DNU Bad Name" } };
describe("MyComponent.vue", () => {
it.only("sanity test", async () => {
const wrapper = shallowMount(MyComponent);
expect(wrapper.vm.isEditable).toBe(true);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
Object.assign(wrapper.vm.myData, data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("myData", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.myData.myObject = data.myObject;
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via return", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.vm.updateMyData(data);
expect(wrapper.vm.isEditable).toBe(false);
});
it.only("updateMyData method via expose🙄", async () => {
const wrapper = shallowMount(MyComponent);
wrapper.__app._container._vnode.component.subTree.component.exposed.updateMyData(
data
);
expect(wrapper.vm.isEditable).toBe(false);
});
});
It is not possible through setData
from the docs:
setData
Updates component internal data.
Signature:
setData(data: Record<string, any>): Promise<void>
Details:
setData does not allow setting new properties that are not defined in the component.
Also, notice that setData does not modify composition API setup() data.
It seems that updating internals with composition API is incompatible with setData. See the method name setData, refers to this.data and was likely kept in the vue test utils mostly for backwards compatibility.
I suspect the theory is that it's bad practice anyway to test, what would be considered, an implementation detail and the component test should focus on validating inputs an outputs only. Fundamentally though, this is a technical issue, because the setup function doesn't expose the refs and reactives created in the setup.
There is a MUCH easier way to do this.....
Put your composables in a separate file
Test the composables stand alone.
Here is the vue file:
<template>
<div>
<div>value: {{ counter }}</div>
<div>isEven: {{ isEven }}</div>
<button type="button" #click="increment">Increment</button>
</div>
</template>
<script setup lang='ts'>
import {sampleComposable} from "./sample.composable";
const {isEven, counter, increment} = sampleComposable();
</script>
Here is the composable:
import {computed, ref} from 'vue';
export function sampleComputed() {
const counter = ref(0);
function increment() {
counter.value++;
}
const isEven = computed(() => counter.value % 2 === 0);
return {counter, increment, isEven};
}
Here is the test:
import {sampleComposable} from "./sample.composable";
describe('sample', () => {
it('simple', () => {
const computed = sampleComposable();
expect(computed.counter.value).toEqual(0);
expect(computed.isEven.value).toEqual(true);
computed.increment();
expect(computed.counter.value).toEqual(1);
expect(computed.isEven.value).toEqual(false);
computed.increment();
expect(computed.counter.value).toEqual(2);
expect(computed.isEven.value).toEqual(true);
})
});
This just 'works'. You don't have to deal w/ mounting components or any other stuff, you are JUST TESTING JAVASCRIPT. It's faster and much cleaner. It seems silly to test the template anyway.
One way to make this easier to test is to put all of your dependencies as arguments to the function. For instance, pass in the props so it's easy to just put in dummy values as need. Same for emits.
You can tests watches as well. You just need to flush the promise after setting the value that is being watched:
composable.someWatchedThing.value = 6.5;
await flushPromises();
Here is my flushPromises (which I found here):
export function flushPromises() {
return new Promise(process.nextTick);
}

React useState() hook returns initial value [duplicate]

I am trying to learn hooks and the useState method has made me confused. I am assigning an initial value to a state in the form of an array. The set method in useState is not working for me, both with and without the spread syntax.
I have made an API on another PC that I am calling and fetching the data which I want to set into the state.
Here is my code:
<div id="root"></div>
<script type="text/babel" defer>
// import React, { useState, useEffect } from "react";
// import ReactDOM from "react-dom";
const { useState, useEffect } = React; // web-browser variant
const StateSelector = () => {
const initialValue = [
{
category: "",
photo: "",
description: "",
id: 0,
name: "",
rating: 0
}
];
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
// const response = await fetch("http://192.168.1.164:5000/movies/display");
// const json = await response.json();
// const result = json.data.result;
const result = [
{
category: "cat1",
description: "desc1",
id: "1546514491119",
name: "randomname2",
photo: null,
rating: "3"
},
{
category: "cat2",
description: "desc1",
id: "1546837819818",
name: "randomname1",
rating: "5"
}
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies);
} catch (e) {
console.error(e);
}
})();
}, []);
return <p>hello</p>;
};
const rootElement = document.getElementById("root");
ReactDOM.render(<StateSelector />, rootElement);
</script>
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
Neither setMovies(result) nor setMovies(...result) works.
I expect the result variable to be pushed into the movies array.
Much like .setState() in class components created by extending React.Component or React.PureComponent, the state update using the updater provided by useState hook is also asynchronous, and will not be reflected immediately.
Also, the main issue here is not just the asynchronous nature but the fact that state values are used by functions based on their current closures, and state updates will reflect in the next re-render by which the existing closures are not affected, but new ones are created. Now in the current state, the values within hooks are obtained by existing closures, and when a re-render happens, the closures are updated based on whether the function is recreated again or not.
Even if you add a setTimeout the function, though the timeout will run after some time by which the re-render would have happened, the setTimeout will still use the value from its previous closure and not the updated one.
setMovies(result);
console.log(movies) // movies here will not be updated
If you want to perform an action on state update, you need to use the useEffect hook, much like using componentDidUpdate in class components since the setter returned by useState doesn't have a callback pattern
useEffect(() => {
// action on update of movies
}, [movies]);
As far as the syntax to update state is concerned, setMovies(result) will replace the previous movies value in the state with those available from the async request.
However, if you want to merge the response with the previously existing values, you must use the callback syntax of state updation along with the correct use of spread syntax like
setMovies(prevMovies => ([...prevMovies, ...result]));
Additional details to the previous answer:
While React's setState is asynchronous (both classes and hooks), and it's tempting to use that fact to explain the observed behavior, it is not the reason why it happens.
TLDR: The reason is a closure scope around an immutable const value.
Solutions:
read the value in render function (not inside nested functions):
useEffect(() => { setMovies(result) }, [])
console.log(movies)
add the variable into dependencies (and use the react-hooks/exhaustive-deps eslint rule):
useEffect(() => { setMovies(result) }, [])
useEffect(() => { console.log(movies) }, [movies])
use a temporary variable:
useEffect(() => {
const newMovies = result
console.log(newMovies)
setMovies(newMovies)
}, [])
use a mutable reference (if we don't need a state and only want to remember the value - updating a ref doesn't trigger re-render):
const moviesRef = useRef(initialValue)
useEffect(() => {
moviesRef.current = result
console.log(moviesRef.current)
}, [])
Explanation why it happens:
If async was the only reason, it would be possible to await setState().
However, both props and state are assumed to be unchanging during 1 render.
Treat this.state as if it were immutable.
With hooks, this assumption is enhanced by using constant values with the const keyword:
const [state, setState] = useState('initial')
The value might be different between 2 renders, but remains a constant inside the render itself and inside any closures (functions that live longer even after render is finished, e.g. useEffect, event handlers, inside any Promise or setTimeout).
Consider following fake, but synchronous, React-like implementation:
// sync implementation:
let internalState
let renderAgain
const setState = (updateFn) => {
internalState = updateFn(internalState)
renderAgain()
}
const useState = (defaultState) => {
if (!internalState) {
internalState = defaultState
}
return [internalState, setState]
}
const render = (component, node) => {
const {html, handleClick} = component()
node.innerHTML = html
renderAgain = () => render(component, node)
return handleClick
}
// test:
const MyComponent = () => {
const [x, setX] = useState(1)
console.log('in render:', x) // ✅
const handleClick = () => {
setX(current => current + 1)
console.log('in handler/effect/Promise/setTimeout:', x) // ❌ NOT updated
}
return {
html: `<button>${x}</button>`,
handleClick
}
}
const triggerClick = render(MyComponent, document.getElementById('root'))
triggerClick()
triggerClick()
triggerClick()
<div id="root"></div>
I know that there are already very good answers. But I want to give another idea how to solve the same issue, and access the latest 'movie' state, using my module react-useStateRef.
As you understand by using React state you can render the page every time the state change. But by using React ref, you can always get the latest values.
So the module react-useStateRef let you use state's and ref's together. It's backward compatible with React.useState, so you can just replace the import statement
const { useEffect } = React
import { useState } from 'react-usestateref'
const [movies, setMovies] = useState(initialValue);
useEffect(() => {
(async function() {
try {
const result = [
{
id: "1546514491119",
},
];
console.log("result =", result);
setMovies(result);
console.log("movies =", movies.current); // will give you the latest results
} catch (e) {
console.error(e);
}
})();
}, []);
More information:
react-usestsateref
I just finished a rewrite with useReducer, following #kentcdobs article (ref below) which really gave me a solid result that suffers not one bit from these closure problems.
See: https://kentcdodds.com/blog/how-to-use-react-context-effectively
I condensed his readable boilerplate to my preferred level of DRYness -- reading his sandbox implementation will show you how it actually works.
import React from 'react'
// ref: https://kentcdodds.com/blog/how-to-use-react-context-effectively
const ApplicationDispatch = React.createContext()
const ApplicationContext = React.createContext()
function stateReducer(state, action) {
if (state.hasOwnProperty(action.type)) {
return { ...state, [action.type]: state[action.type] = action.newValue };
}
throw new Error(`Unhandled action type: ${action.type}`);
}
const initialState = {
keyCode: '',
testCode: '',
testMode: false,
phoneNumber: '',
resultCode: null,
mobileInfo: '',
configName: '',
appConfig: {},
};
function DispatchProvider({ children }) {
const [state, dispatch] = React.useReducer(stateReducer, initialState);
return (
<ApplicationDispatch.Provider value={dispatch}>
<ApplicationContext.Provider value={state}>
{children}
</ApplicationContext.Provider>
</ApplicationDispatch.Provider>
)
}
function useDispatchable(stateName) {
const context = React.useContext(ApplicationContext);
const dispatch = React.useContext(ApplicationDispatch);
return [context[stateName], newValue => dispatch({ type: stateName, newValue })];
}
function useKeyCode() { return useDispatchable('keyCode'); }
function useTestCode() { return useDispatchable('testCode'); }
function useTestMode() { return useDispatchable('testMode'); }
function usePhoneNumber() { return useDispatchable('phoneNumber'); }
function useResultCode() { return useDispatchable('resultCode'); }
function useMobileInfo() { return useDispatchable('mobileInfo'); }
function useConfigName() { return useDispatchable('configName'); }
function useAppConfig() { return useDispatchable('appConfig'); }
export {
DispatchProvider,
useKeyCode,
useTestCode,
useTestMode,
usePhoneNumber,
useResultCode,
useMobileInfo,
useConfigName,
useAppConfig,
}
With a usage similar to this:
import { useHistory } from "react-router-dom";
// https://react-bootstrap.github.io/components/alerts
import { Container, Row } from 'react-bootstrap';
import { useAppConfig, useKeyCode, usePhoneNumber } from '../../ApplicationDispatchProvider';
import { ControlSet } from '../../components/control-set';
import { keypadClass } from '../../utils/style-utils';
import { MaskedEntry } from '../../components/masked-entry';
import { Messaging } from '../../components/messaging';
import { SimpleKeypad, HandleKeyPress, ALT_ID } from '../../components/simple-keypad';
export const AltIdPage = () => {
const history = useHistory();
const [keyCode, setKeyCode] = useKeyCode();
const [phoneNumber, setPhoneNumber] = usePhoneNumber();
const [appConfig, setAppConfig] = useAppConfig();
const keyPressed = btn => {
const maxLen = appConfig.phoneNumberEntry.entryLen;
const newValue = HandleKeyPress(btn, phoneNumber).slice(0, maxLen);
setPhoneNumber(newValue);
}
const doSubmit = () => {
history.push('s');
}
const disableBtns = phoneNumber.length < appConfig.phoneNumberEntry.entryLen;
return (
<Container fluid className="text-center">
<Row>
<Messaging {...{ msgColors: appConfig.pageColors, msgLines: appConfig.entryMsgs.altIdMsgs }} />
</Row>
<Row>
<MaskedEntry {...{ ...appConfig.phoneNumberEntry, entryColors: appConfig.pageColors, entryLine: phoneNumber }} />
</Row>
<Row>
<SimpleKeypad {...{ keyboardName: ALT_ID, themeName: appConfig.keyTheme, keyPressed, styleClass: keypadClass }} />
</Row>
<Row>
<ControlSet {...{ btnColors: appConfig.buttonColors, disabled: disableBtns, btns: [{ text: 'Submit', click: doSubmit }] }} />
</Row>
</Container>
);
};
AltIdPage.propTypes = {};
Now everything persists smoothly everywhere across all my pages
React's useEffect has its own state/lifecycle. It's related to mutation of state, and it will not update the state until the effect is destroyed.
Just pass a single argument in parameters state or leave it a black array and it will work perfectly.
React.useEffect(() => {
console.log("effect");
(async () => {
try {
let result = await fetch("/query/countries");
const res = await result.json();
let result1 = await fetch("/query/projects");
const res1 = await result1.json();
let result11 = await fetch("/query/regions");
const res11 = await result11.json();
setData({
countries: res,
projects: res1,
regions: res11
});
} catch {}
})(data)
}, [setData])
# or use this
useEffect(() => {
(async () => {
try {
await Promise.all([
fetch("/query/countries").then((response) => response.json()),
fetch("/query/projects").then((response) => response.json()),
fetch("/query/regions").then((response) => response.json())
]).then(([country, project, region]) => {
// console.log(country, project, region);
setData({
countries: country,
projects: project,
regions: region
});
})
} catch {
console.log("data fetch error")
}
})()
}, [setData]);
Alternatively, you can try React.useRef() for instant change in the React hook.
const movies = React.useRef(null);
useEffect(() => {
movies.current='values';
console.log(movies.current)
}, [])
The closure is not the only reason.
Based on the source code of useState (simplified below). Seems to me the value is never assigned right away.
What happens is that an update action is queued when you invoke setValue. And after the schedule kicks in and only when you get to the next render, these update action then is applied to that state.
Which means even we don't have closure issue, react version of useState is not going to give you the new value right away. The new value doesn't even exist until next render.
function useState(initialState) {
let hook;
...
let baseState = hook.memoizedState;
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;
do {
const action = firstUpdate.action;
baseState = action(baseState); // setValue HERE
firstUpdate = firstUpdate.next;
} while (firstUpdate !== hook.queue.pending);
hook.queue.pending = null;
}
hook.memoizedState = baseState;
return [baseState, dispatchAction.bind(null, hook.queue)];
}
function dispatchAction(queue, action) {
const update = {
action,
next: null
};
if (queue.pending === null) {
update.next = update;
} else {
update.next = queue.pending.next;
queue.pending.next = update;
}
queue.pending = update;
isMount = false;
workInProgressHook = fiber.memoizedState;
schedule();
}
There's also an article explaining the above in the similar way, https://dev.to/adamklein/we-don-t-know-how-react-state-hook-works-1lp8
I too was stuck with the same problem. As other answers above have clarified the error here, which is that useState is asynchronous and you are trying to use the value just after setState. It is not updating on the console.log() part because of the asynchronous nature of setState, it lets your further code to execute, while the value updating happens on the background. Thus you are getting the previous value. When the setState is completed on the background it will update the value and you will have access to that value on the next render.
If anyone is interested to understand this in detail. Here is a really good Conference talk on the topic.
https://www.youtube.com/watch?v=8aGhZQkoFbQ
I found this to be good. Instead of defining state (approach 1) as, example,
const initialValue = 1;
const [state,setState] = useState(initialValue)
Try this approach (approach 2),
const [state = initialValue,setState] = useState()
This resolved the rerender issue without using useEffect since we are not concerned with its internal closure approach with this case.
P.S.: If you are concerned with using old state for any use case then useState with useEffect needs to be used since it will need to have that state, so approach 1 shall be used in this situation.
If we have to update state only, then a better way can be if we use the push method to do so.
Here is my code. I want to store URLs from Firebase in state.
const [imageUrl, setImageUrl] = useState([]);
const [reload, setReload] = useState(0);
useEffect(() => {
if (reload === 4) {
downloadUrl1();
}
}, [reload]);
const downloadUrl = async () => {
setImages([]);
try {
for (let i = 0; i < images.length; i++) {
let url = await storage().ref(urls[i].path).getDownloadURL();
imageUrl.push(url);
setImageUrl([...imageUrl]);
console.log(url, 'check', urls.length, 'length', imageUrl.length);
}
}
catch (e) {
console.log(e);
}
};
const handleSubmit = async () => {
setReload(4);
await downloadUrl();
console.log(imageUrl);
console.log('post submitted');
};
This code works to put URLs in state as an array. This might also work for you.
With custom hooks from my library, you can wait for the state values to update:
useAsyncWatcher(...values):watcherFn(peekPrevValue: boolean)=>Promise - is a promise wrapper around useEffect that can wait for updates and return a new value and possibly a previous one if the optional peekPrevValue argument is set to true.
(Live Demo)
import React, { useState, useEffect, useCallback } from "react";
import { useAsyncWatcher } from "use-async-effect2";
function TestComponent(props) {
const [counter, setCounter] = useState(0);
const [text, setText] = useState("");
const textWatcher = useAsyncWatcher(text);
useEffect(() => {
setText(`Counter: ${counter}`);
}, [counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
setCounter((counter) => counter + 1);
const updatedText = await textWatcher();
console.log(updatedText);
})();
}, []);
return (
<div className="component">
<div className="caption">useAsyncEffect demo</div>
<div>{counter}</div>
<button onClick={inc}>Inc counter</button>
</div>
);
}
export default TestComponent;
useAsyncDeepState is a deep state implementation (similar to this.setState (patchObject)) whose setter can return a promise synchronized with the internal effect. If the setter is called with no arguments, it does not change the state values, but simply subscribes to state updates. In this case, you can get the state value from anywhere inside your component, since function closures are no longer a hindrance.
(Live Demo)
import React, { useCallback, useEffect } from "react";
import { useAsyncDeepState } from "use-async-effect2";
function TestComponent(props) {
const [state, setState] = useAsyncDeepState({
counter: 0,
computedCounter: 0
});
useEffect(() => {
setState(({ counter }) => ({
computedCounter: counter * 2
}));
}, [state.counter]);
const inc = useCallback(() => {
(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
await setState(({ counter }) => ({ counter: counter + 1 }));
console.log("computedCounter=", state.computedCounter);
})();
});
return (
<div className="component">
<div className="caption">useAsyncDeepState demo</div>
<div>state.counter : {state.counter}</div>
<div>state.computedCounter : {state.computedCounter}</div>
<button onClick={() => inc()}>Inc counter</button>
</div>
);
}
var [state,setState]=useState(defaultValue)
useEffect(()=>{
var updatedState
setState(currentState=>{ // Do not change the state by get the updated state
updateState=currentState
return currentState
})
alert(updateState) // the current state.
})
Without any addtional NPM package
//...
const BackendPageListing = () => {
const [ myData, setMyData] = useState( {
id: 1,
content: "abc"
})
const myFunction = ( x ) => {
setPagenateInfo({
...myData,
content: x
})
console.log(myData) // not reflecting change immediately
let myDataNew = {...myData, content: x };
console.log(myDataNew) // Reflecting change immediately
}
return (
<>
<button onClick={()=>{ myFunction("New Content")} }>Update MyData</button>
</>
)
Not saying to do this, but it isn't hard to do what the OP asked without useEffect.
Use a promise to resolve the new state in the body of the setter function:
const getState = <T>(
setState: React.Dispatch<React.SetStateAction<T>>
): Promise<T> => {
return new Promise((resolve) => {
setState((currentState: T) => {
resolve(currentState);
return currentState;
});
});
};
And this is how you use it (example shows the comparison between count and outOfSyncCount/syncCount in the UI rendering):
const App: React.FC = () => {
const [count, setCount] = useState(0);
const [outOfSyncCount, setOutOfSyncCount] = useState(0);
const [syncCount, setSyncCount] = useState(0);
const handleOnClick = async () => {
setCount(count + 1);
// Doesn't work
setOutOfSyncCount(count);
// Works
const newCount = await getState(setCount);
setSyncCount(newCount);
};
return (
<>
<h2>Count = {count}</h2>
<h2>Synced count = {syncCount}</h2>
<h2>Out of sync count = {outOfSyncCount}</h2>
<button onClick={handleOnClick}>Increment</button>
</>
);
};
Use the Background Timer library. It solved my problem.
const timeoutId = BackgroundTimer.setTimeout(() => {
// This will be executed once after 1 seconds
// even when the application is the background
console.log('tac');
}, 1000);
// replace
return <p>hello</p>;
// with
return <p>{JSON.stringify(movies)}</p>;
Now you should see, that your code actually does work. What does not work is the console.log(movies). This is because movies points to the old state. If you move your console.log(movies) outside of useEffect, right above the return, you will see the updated movies object.

Resources