Single useSelector instead of multiple useSelector - javascript

I am working on React web app using typescript, hooks. Each component
must have only one useSelector(), but actually I have 2 useSelector(),
Is there any solution for 2 useSelector() to be merged or something
else.
export default function UserInfo() {
const { city1, city2, city3 } = useSelector(cityNameSelector);
const { country } = useSelector(countryNameSelector);
.....
...
}

Related

Do I need to import Pinia store props multiple times in a Vue component?

I am working on my first Vue project. I'm used to React and vanilla js, but just getting my head around a few concepts in Vue here.
In particular, importing state and action props from a Pinia store, and seemingly having to import those multiple times in a single Vue component (something I don't need to do in React).
In this example, I am importing a simple count value, and an increment function, and trying to use these in a few different places:
<script setup>
// I import everything initially in setup, which works fine,
// and these props (currentCount and incrementCount)
// can be used in my template:
import { storeToRefs } from 'pinia';
import { useStore } from '#/stores/store';
const { currentCount } = storeToRefs(useStore());
const { incrementCount } = useStore();
</script>
<template>
<main>
Current count: {{ currentCount }}
<button #click="incrementCount">Increment</button>
</main>
</template>
<script>
// I can't use store values from setup here.
// This doesn't work:
// console.log(currentCount);
// I also can't import store values here.
// I get the following error:
// "getActivePinia was called with no active Pinia"
// const { currentCount } = storeToRefs(useStore());
export default {
mounted() {
// I have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
watch: {
// weirdly, this reference to watching "currentCount" works:
currentCount() {
// I also have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
},
};
</script>
As you can see, if I want to use store values in my template, on mount, and in a watcher (whereby I'd use React's useEffect hook) I am having to import the store props 3 times in total.
Is this correct / normal? Is there a simpler way to achieve what I'm doing, where I only import props once? I want to be sure I haven't missed something and am not doing something in an unusual way.
Thanks for any help and advice!
Pinia was designed with Composition API in mind.
So its intended usage is inside setup() function, where you'd only import it once.
To use it outside of a setup() function, you have two main routes:
inside components, you can just return it from setup() and it becomes available in any hook/method/getter. Either as this.store or spread:
import { useStore } from '#/store'
import { toRefs } from 'vue'
// or from '#vue/composition-api' in Vue2
export default {
setup: () => ({ ...toRefs(useStore()) })
}
/* this makes every state prop, getter or action directly available
on current component instance. In your case, `this.currentCount`.
Obviously, you can also make the entire store available as `this.someStore`:
setup: () => ({ someStore: useSomeStore() })
// now you can use `this.someStore` anywhere
*/
a more general approach is to export the pinia instance (returned by createPinia()), from main.(js|ts), import it where you need the store and then call useStore() passing the pinia instance as an argument.
This can be done anywhere, even outside of components.
Generic example:
import { pinia } from 'main.js'
import { useSomeStore } from '#/store'
const someStore = useSomeStore(pinia);
I should probably also mention the mapState helper provided by pinia. It allows you to select only a few of the keys exposed to current instance. Example:
import { mapState } from 'pinia'
// ...
computed: {
...mapState(useSomeStore, [ 'currentCount'])
}
// Now `this.currentCount` is available
Note: mapState is weirdly named, as it allows you to access more than just state props (also getters and actions). It was named mapState to match the similar helper from vuex.
An even more general approach is to add your store as global, using the plugin registration API in Vue2:
import { useSomeStore } from '#/store';
import { createPinia } from 'pinia';
const pinia = createPinia();
const someStorePlugin = {
install(Vue, options) {
Vue.prototype.someStore = useSomeStore(options.pinia)
}
};
Vue.use(someStorePlugin, { pinia });
new Vue({ pinia });
After this, every single component of your Vue instance will have this.someStore available on it, without you needing to import it.
Note: I haven't tested adding a store in globals (and I definitely advise against it - you should avoid globals), but i expect it to work.
If you want to combine pinia stores with the options API, one way to do it is to use the setup() function inside the options to call useStore:
<script>
import { useStore } from '#/stores/store';
export default {
setup() {
const store = useStore();
return {store}
},
watch: {
store.currentBrightness(newVal, oldVal){
// your code
}
},
methods: {
// inside methods use this.store
},
mounted() {
console.log(this.store.currentCount);
}
}
</script>
Some might consider this as a unwanted mix of composition and options API, but in my view it is a quite good solution for pinia stores.
Nechoj, has the most straightforward answer. Also if you have multiple stores you can always import the stores as necessary into a parent component then use inject just add some parts. For example I have a route data that is called via an api, I don't need it everywhere all the time so i call it in a parent then use inject to use those routes in a drop down that might be a great grandchild component. I don't need that whole utils store just the routes.
index page:
import { useUtilsStore } from "src/stores/utilsStore";
const passengerRoutes = computed(() => utilsStore.getPassengerRoutes);
provide("passengerRoutes", passengerRoutes);
grandchild component:
const compRoutes = inject("passengerRoutes");

Add useContext() on JSON export

I want to create a list of functionalities that I can access easily from anywhere on my app by simply importing the component. Here is what my component looks like:
functionalities.component.jsx
import { AES } from "crypto-js"
import { useContext, useState } from "react"
import { ConfigurationContext } from "../env"
const {configurationState} = useContext(ConfigurationContext);
const Functionalities = {
encrypt: (info) => AES.encrypt(info, configurationState.application.key).toString();
}
export default Functionalities
The problem I'm facing now is that I'm not able to use any context values since it would cause an error. Is there a way to implement "useContext" on this?
You can call a React Hook only inside a React component or inside a custom hook, it's one of the rules of the hooks.
The best you could do, if you need to share common functionalities, is creating a set of custom hooks.
import { AES } from "crypto-js"
import { useContext } from "react"
import { ConfigurationContext } from "../env"
const Functionalities = {
useEncrypt: () => {
const { configurationState } = useContext(ConfigurationContext);
return (info) => AES.encrypt(info, configurationState.application.key).toString();
}
};
export default Functionalities;
Example usage (always remember to call useContext inside a Context.Provider).
function EncryptComponent({info}) {
const encrypt = Functionalities.useEncrypt();
return <button onClick={() => encrypt(info)}>Encrypt</button>
}
I provide a CodeSandbox example that show how to do that.

Modify nested state with Redux Reducer

I run into a problem that is litterally blowing my mind.
I'm developing my web application using React and Redux, my application use a system of notification implemented with Firebase.
Every notification is structured as below:
var notification = {
from_user:{
name: 'John',
surname: 'Doe'
},
payload:{
message:'Lorem ipsum ...'
}
seen:false,
timestamp:1569883567
}
After fetched, notification is send to notificationReducer with:
dispatch({type:'FETCH_NOTIFICATION_OK',payload:notification})
And so far everything is ok.
My notificationReducer is structured as below:
const INITIAL_STATE = {
loading:false,
notification:{}
}
const notificationReducer = (state=INITIAL_STATE,action)=>{
switch(action.type){
case 'FETCHING_NOTIFICATION':
return {...state,loading:true}
case 'FETCH_NOTIFICATION_OK':
return {...state,loading:false,notification:action.payload} // I THINK PROBLEM IS HERE
default:
return state
}
}
export default notificationReducer;
The problem is that, when I pass state props to my presentational component, notification object is empty
My presentational component is reported below
import React from 'react';
import {getNotification} from '../actions/actioncreators';
import {connect} from 'react-redux';
class NotificationDetail extends React.Component {
componentWillMount(){
this.props.fetch_notification('9028aff78d78x7hfk');
console.log(this.props.notification) // IT PRINTS: {}
}
render(){
return(
<div>
'TODO'
</div>
)
}
}
const mapStateToProps = state =>{
return {
is_loading:state.notificationReducer.loading,
notification:state.notificationReducer.notification
}
}
const mapDispatchToProps = dispatch =>{
return {
fetch_notification: (id_notification)=>dispatch(getNotification(id_notification))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(NotificationDetail)
Any suggestion ?
EDIT: In my reducer I tried to print the new state. I succesfully got this:
But, Anyway In my presentational component I got an empty object
I think the dispatch call hasn't fired yet. Try executing the below
componentDidMount() {
this.props.fetch_notification();
}
render() {
console.log(this.props.notification); // It should print an output here if your xhr/ajax/axios call is correct
}
Also, using componentWillMount is UNSAFE (according to the ReactJS current documentation). Avoid using this lifecycle in the future.

How can I test areStatesEqual in a redux connected component

I have a redux connected component that is utilising areStatesEqual in the options argument in the redux connect api to avoid re-rendering.
shouldComponentUpdate does the same job, but a lot slower in my use case.
I'm struggling to test multiple state updates, asserting that under certain conditions this component believes the states are equal, and shouldn't re-render.
I'm using react-mock-store for other components, but that only deals with static state, not dynamic. Also using mocha & enzyme elsewhere too.
Does anyone have any ideas how I can test this behaviour?
(This is a simplified version of a more complicated component)
import { connect } from 'react-redux';
import { compose } from 'redux';
import MyComponent from './my-component';
const mapStateToProps = (state, props) => ({
text: state.a.text[props.id],
});
const areStatesEqual = (next, prev) => {
return next.a === prev.a;
};
const options = { areStatesEqual };
export default connect(mapStateToProps, undefined, undefined, options)(MyComponent);
MyComponent is a simple functional component that just accepts props and renders:
export default function Label({ text }) {
return <p>{text}</p>;
}
In the end I managed to test the logic by exporting the areStatesEqual function and doing a simple test.
export const areStatesEqual = (next, prev) => {
return next.a === prev.a;
};
I haven't figured out a way to test the flow of the lifecycle of a component

images being duplicated when navigating between pages in redux / react application

I am creating a react / redux application for learning purposes. I am attempting to pull the images from contentful through their api. I have set up an action, reducer and component which displays the image fine, but when navigating between pages the images are duplicated. Everytime I return to the same page the image is duplicated + 1 so if I visit the page five times the image will exist 5 times on that page.
It would be great if anyone could give me some pointers in how to debug this or even a solution to the issue.
action
export function fetchAsset(id) {
const request = axios.get(`${API_BASE_URL}/spaces/${API_SPACE_ID}/assets/${id}?access_token=${API_TOKEN}`);
return {
type: FETCH_ASSET,
payload: request
};
}
reducer
import { FETCH_ASSET } from '../actions/index';
const EMPTY_ARRAY = []
export default function(state = EMPTY_ARRAY, action) {
switch(action.type) {
case FETCH_ASSET:
return [ ...state, action.payload.data];
default:
return state;
}
}
asset component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchAsset } from '../actions/index';
import styled, { css } from 'styled-components';
const RespImg = styled.img`
width: 100%;
`
class Asset extends Component {
componentWillMount() {
this.props.fetchAsset(this.props.assetId)
}
shouldComponentUpdate(nextProps, nextState) {
return true;
}
renderAsset() {
var assetArray = this.props.assets;
console.log(assetArray + ' this.props')
return assetArray.map((asset, index) => {
if (asset.sys.id == this.props.assetId) {
return (
<RespImg src={asset.fields.file.url} alt={asset.fields.file.fileName} key={index}/>
);
}
});
}
render() {
return (
<div className="asset">
{this.renderAsset()}
</div>
);
}
}
function mapStateToProps(state) {
return {
assets: state.assets
};
}
export default connect(mapStateToProps, { fetchAsset })(Asset)
Adding the component to the page
<Asset assetId={work.fields.featuredImage.sys.id} assetKey={index} />
I believe the problem here is that you fetch this image several times, that is why it is all good during the first render.
You keep your images in the array, and maybe you download the same asset, and add it to the array, even though it might exist there already. For such entities, the technique called normalizing can be used, so your state will look like:
state = {
[id]: Asset
};
Using this technique, you can get needed asset by id (you have it probably from the URL parameter).
Arrays in reducers are usually used for collections – for example, if you want to fetch all your assets. You can normalize response, and keep entities by id in one reducer, and result of collection requests in another one – so you'll have an array with ids, and an object with all possible Assets.
One more thing – #Dyo recommended you to put something into key, like id or url, and it is a good advice. However, if you open your console, you'll probably see something about elements with the same key. Basically, react does not render elements with the same key, so probably, your array of the same entities was rendered, but react rendered only one – all others were discarded.

Categories