Structuring state for a react/redux app - javascript

I'm struggling with structuring a React/Redux application - I'm listing out the problem with options I tried for solutions, but nothing "feels right", so hoping someone here could help me out.
Here's a rough idea of my component structure:
<Dashboard>
<Widget1 dataFetcher=()=>{}>
<Header>
<Title> ... </Title>
<Menu>
<MenuItem {..cosmeticProps} text="OpenSettings" onClick=handleSettingsOpen>
<MenuItem {..cosmeticProps} text="Delete" onClick=handleWidgetDelete>
</Menu>
</Header>
<Body>
<Settings isOpen isValid fields onValidate onAutoComplete.. </Settings>
{ ifError ? ErrorLayout}
{ ifFetching ? FetchingLayout }
{ ifValid ? DataLayout }
</Body>
</Widget1>
...
</Dashboard>
And here's the state structure (event handlers shown for completeness, not because they're explicitly part of the state)
Dash: {
widgets: {
widget1: {
menu: {
isOpen: true,
handleSettingsOpen: ()=>{}
handleWidgetDelete: ()=>{}
}
settings: {
isOpen: true,
isValid: true,
fields: [...],
onValidate: ()=>{},
onAutoComplete:()=>{},
onSave:()=>{}
}
data: {
isFetching: false,
isError: false,
items: [],
fetch: ()=>{}
parse: ()=>{}
}
}
...
}
}
Option 1:
Connect the dashboard and let it pass to children as required. i.e.,
Connected-dashboard.js
stateToProps ()=> { widgets: state.widgets }
dispatchToProps ()=> { handleSettingsOpen, handleWidgetDelete handleSettingsSave ... } //Dashboard would bind these with moduleid while rendering
Pro: Everything else can be 'dumb', single source of truth
Con: Knows too much about state, list of props/dispatches it takes just to pass down makes for ugly reading
Option 2:
Build a 'connected' widget and use that in the dashboard.
connected-widget.js
stateToProps ()=> { state.widgets[props.widgetid] }
dispatchToProps ()=> { handleSettingsOpen, handleWidgetDelete handleSettingsSave ... }
Pro: Dashboard can now be a dumb container, which it is anyway
Con: Widget knows too much about state structure?
Option 3:
Build connected versions of individual components and assemble later
connected-menu.js
stateToProps ()=> { state.widgets[props.widgetid].menu }
dispatchToProps ()=> { handleSettingsOpen, handleWidgetDelete }
connected-settings.js
stateToProps ()=> { state.widgets[props.widgetid].settings }
dispatchToProps ()=> { handleSave, handleValidate }
Pro: Every component gets exactly the slice of state it cares about
Con: Too many components listening on the state? Also the question of who 'assembles' it.
Option 3.1:
Restructure state to be:
Dashboard: {
widgets: { ..}
menu: {widgetid: {isopen ..}}
settings: {widgetid: {widgetid ..}}
}
(State is flatter with this approach, but not sure if it matters much)
Overall, this may be naive/obvious, but to me the trade-off seems to be having a parent which either knows too much about the state, or too much about how it's children are put-together. How would you approach this?

Option 3: Does it make sense for Menu and Settings to know "widgetId"? It seems they would be more reusable if they simply receive the properties menu or settings respectively.
Option 1: Do you want to update Dashboard stateToProps and dispatchToProps for each widget component supported?
For these reasons, I like option 2, the connected Widget1.
As for state nesting depth, Redux Async Actions has a "Note on Nested Entities" that suggests avoiding deeply nested entities to avoid duplicate data.
In your example, if any widgets had duplicate menu or settings state object, a normalized state would allow the widgets to share the same state.
Dashboard: {
widgets: {
widget1: {menuId:1, settingsId: 1, ...},
widget2: {menuId:1, settingsId: 1, ...},
},
menus: {1: {...}},
settings: {1: {...}}
}
Actually, with this structure, Menu and Settings only need to know menuId or settingsId, not widgetId. I still prefer connecting the widget though.

Related

Adding attribute for conditionally rendering a component in VueJS

In our frontend, we want to disable certain elements (i.e. buttons) depending on whether a user has "permissions" to trigger an action or not. To keep our code clean, we would like to do something like this:
<button reqPermission="edit">
This should call a JS method connecting to our permission service and Vue should only render the element / enable it if the request is truthy.
I don't have much clue of VueJS - what I would like to avoid though, is using something like v-if="...", since this would clutter our code. Any hints on how to implement such a "custom attribute" that influences the rendering of a component would be highly appreciated. What I found so far is https://forum.vuejs.org/t/how-to-conditionally-render-a-component/69687/6
If you want to disable it based on a boolean you can do this inside the Vue template syntax:
<button :disabled="!userRights.edit">
If you want to prevent rendering instead of disabling the button, use v-if instead of :disabled.
If the boolean has to be loaded from a backend, I'd recommend using a vuex store action to fetch the user rights and conditional rendering based on the store state:
<template>
<button :disabled="!isLoading && !userRights.edit">
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'buttonComponent',
computed: {
...mapState('userModule', ['isLoading', 'userRights']),
},
mounted() {
this.$store.dispatch('userModule/getRights')
},
}
</script>
export default {
namespaced: true,
state: {
isLoading: false,
userRights: {
edit: false,
view: false,
},
},
mutations: {
updateIsLoading(state, isLoading) {
state.isLoading = isLoading
},
updateUserRights(state, userRights) {
state.userRights = userRights
},
},
actions: {
async getRights() {
this.commit('userModule/updateIsLoading', true)
const user = await getCurrentUserID()
const rights = await getRightsByUser(user) // backend call
this.commit('userModule/updateUserRights', rights)
this.commit('userModule/updateIsLoading', false)
},
},
}

How to make a toggler to translate a site using redux

I've been working for the past two days on a task which is adding the capability to translate a whole website from English to Spanish when the user selects the toggle button, However, I'm really new into Redux (use it once on a completely different project). The people who gave me the code already configured the reducers, I just need to read the status on each component.
I've tried using this code on one of the components:
const store = createStore(reducer);
store.dispatch({
type: 'TOGGLE-LANG'
});
store.subscribe(() => console.log(store.getState()));
However, it is still has something missing, and at this point, I'm completely lost and I would love to have some guidance to know what to do.
I created a Gist with all the code involved in this task, It has commented on what's the expected behavior
[Gist Link] : https://gist.github.com/ManudeQuevedo/12cd63cf7431b5ec9b982a37391b7c56
Currently, there are no errors, it is recognizing the reducer (Lang), but I would love to know how to make it actionable in the other components that need to be translated. Thanks in advance!
You can use i18n. Replacing the text contents on your website to keys, and adding a map matching the keys to different languages.
For example, before you have
<div>
name:
</div>
Now it will be like:
<div>
{t('name')}
</div>
Here is the link of an example project
At the end this is what I did:
lang.js (reducer)
import { fromJS } from 'immutable';
const initState = fromJS({
value: 'en',
translations: {
en: {
component: {
title: 'Mobile Connectivity',
subtitle: 'Smart Messaging'
}
},
es: {
component: {
title: 'Conectividad Móvil',
subtitle: 'Mensajería Inteligente'
}
},
}
});
export default (state = initState, action) => {
switch (action.type) {
case "CHANGE_LANGUAGE":
return {
...state,
value: action.payload
};
default:
return state;
}
};
and I implemented it by destructuring it on this component like so:
[Gist][1]
I added it to a gist because it's a long code so it would be more readable like that.

How to share "computed" methods across a Vue.js application

I have a Vue.js application which loads a list of items, and each item is passed as a prop to a Vue component.
I figured out that by using mixins I can share common component properties, like computed,created, etc.
Now, I'm trying to sort the list of items and can't figure out how I would access each component's computed properties to apply sorting/filtering. How can I accomplish this?
Items
[{
price: 10,
qty: 2
}, {
price: 8,
qty: 3
}]
Mixin - ./Cost.js
export default {
computed: {
cost () {
return this.price * this.qty;
}
}
}
Component (which works as expected) - ./Product.vue
import Cost from './Cost.js'
export default {
name: 'product-item',
props: ['product'],
mixins: [Cost]
}
How would you access the computed properties, or restructure this setup?
List component
<template>
<div id="list">
<div v-for="product in sorted" :product="product">Cost: {{ cost }} </div>
</div>
</template>
<script>
import ProductItem from './Product.vue'
export default {
components: { ProductItem },
created: () {
this.items = [...] // as noted above
},
computed: {
sorted () {
return this.items.sort( (a,b) => b.cost - a.cost); // cost is not accessible!
}
}
}
</script>
Use vuex. Your vuex store will provide a getters object that can be wrapped into multiple components’ native computed objects, or accessed directly. Your code will be DRY, reactive, cached, and maintainable.
From my experience, once you need to go beyond child-parent data relationships, vuex, store, and shared state are the way to go. Once you get the hang of it, it is downright magical how your app evolves.
It is beyond scope of the question to show how to install vuex. Visit https://vuex.vuejs.org/guide/getters.html to see how getters are similar to computed properties, with the value of being shared between components. The official Vuex guide will also demonstrate how to initialize your Vue instance with the store.
Here are some snippets to show you the actors in the vuex system.
Store and State
// state definition (basically a shared reactive 'data' object that lives outside components)
state:{
message:'Hello'
}
// the store getters are declared as methods and accessed as properties (just like component/computed)
getters:{
message: state => return state.message
}
Accessing From Components
// component 1 wraps getter
computed:{
message(){
return this.$store.getters.message
}
}
// component 2 also wraps getter
computed:{
message(){
return this.$store.getters.message
}
}
// templates can also use getters directly
<div>{{$store.getters.message}}</div>
// If message was wrapped, you can simply use the computed property
<div>{{message}}</div>
Once you start using vuex, all sorts of other treasures start to emerge, such as the developer tools in Chrome, undo/redo support, simple refactoring of state, time-travel debugging, app persistence, etc. There are also shortcuts for adding multiple store getters into your computed properties.
As suggested by #Sphinx, you could use a ref to access the child component.
For example:
<template>
<div id="list">
<product-item v-for="product in sorted" :product="product" :ref="product"></product-item>
</div>
</template>
<script>
import ProductItem from './Product.vue'
export default {
components: { ProductItem },
data: () => ({
hidrated: false,
items: []
})
created() {
this.items = [...] // as noted above
},
mounted() {
this.hidrated = true
},
computed: {
sorted () {
if (!this.hidrated && !Object.keys(this.$refs).length) {
// handle initial state, before rendered
return this.items
}
return Object.values(this.$refs)[0]
.sort((a,b) => b.cost - a.cost)
.map(c => c.product)
}
}
}
</script>
This is assuming you have no other ref in your List Component.
You also have to check if the component is rendered first, here I use hidrated to flag when the component is mounted.

Throttle or debounce async calls in Vue 2 while passing arguments to debounced function

I have a Vue 2 application that uses an array of objects to back a search/multiselect widget provided by vue-multiselect.
I have looked at the Vue 1 -> 2 migration guide on debouncing calls, but the example they give did not propagate the arguments from the DOM elements to the business logic.
Right now the select fires change events with every keystroke, but I would like to throttle this (EG with lodash#throttle) so I'm not hitting my API every few milliseconds while they're typing.
import {mapGetters} from 'vuex';
import { throttle } from 'lodash';
import Multiselect from 'vue-multiselect'
export default {
components: {
Multiselect
},
data() {
return {
selectedWork: {},
works: [],
isLoading: false
}
},
computed: {
...mapGetters(['worksList']),
},
methods: {
getWorksAsync: throttle((term) => {
// the plan is to replace this with an API call
this.works = this.worksList.filter(work => titleMatches(work, term));
}, 200)
}
}
Problem: when the user types in the select box, I get the error:
TypeError: Cannot read property 'filter' of undefined
which is happening because this.worksList is undefined inside the throttle function.
Curiously, when I use the dev tools debugger, this.worksList has the value I need to dereference, with this referring to the Vue component.
Currently I am not calling the API from within the component, but the problem remains the same:
How can I throttle this call, and have the proper this context to update my this.works list? EDIT: this is explained in Vue Watch doesnt Get triggered when using axios
I also need to capture the user's query string from the multiselect widget to pass to the API call.
What is the proper pattern in Vue 2?
I ran into the same issue when using lodash.debounce. I'm a huge fan of arrow syntax, but I discovered that it was causing _.throttle() and _.debounce(), etc. to fail.
Obviously my code differs from yours, but I have done the following and it works:
export default {
...,
methods: {
onClick: _.debounce(function() {
this.$emit('activate', this.item)
}, 500)
}
}
Even though I'm not using arrow syntax here, this still references the component inside the debounced function.
In your code, it'd look like this:
export default {
...,
methods: {
getWorksAsync: throttle(function(term) {
// the plan is to replace this with an API call
this.works = this.worksList.filter(work => titleMatches(work, term));
}, 200)
}
}
Hope that helps!
I was unable to find an answer on SO (or anywhere) for this, but I eventually cobbled it together through trial and error, and from related materials here and in the docs.
Things that work that I didn't do, and why
It is possible to get get the value directly using a JavaScript DOM query, and it is also possible to dig in to the multiselect component's structure and get the value. The first solution circumvents the framework, the second depends on undocumented attributes of the multiselect component. I am avoiding both of those solutions as non-idiomatic and brittle.
My current solution
Updated an attribute on the component whenever there was a change event in the search box. This allowed me to capture the user's query string.
Called my throttled async function from inside the event listener.
Passed a regular function instead of an arrow function to throttle, which gave the correct this (the Vue component.)
If anyone has a suggestion for a better way to do this in Vue 2, I'm all ears.
Here's what my solution looked like in the end:
<template>
<div>
<label
class="typo__label"
for="ajax">Async select</label>
<multiselect
id="ajax"
v-model="selectedWork"
label="title"
track-by="id"
placeholder="Type to search"
:options="works"
:searchable="true"
:loading="isLoading"
:internal-search="false"
:multiple="false"
:clear-on-select="true"
:close-on-select="true"
:options-limit="300"
:limit="3"
:limit-text="limitText"
:max-height="600"
:show-no-results="false"
open-direction="bottom"
#select="redirect"
#search-change="updateSearchTerm">
<span slot="noResult">Oops! No elements found. Consider changing the search query.</span>
</multiselect>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
import { throttle } from 'lodash';
import Multiselect from 'vue-multiselect'
export default {
components: {
Multiselect
},
data() {
return {
searchTerm: '',
selectedWork: {},
works: [],
isLoading: false
}
},
computed: {
...mapGetters(['worksList']),
},
methods: {
limitText(count) {
return `and ${count} other works`;
},
redirect(work) {
// redirect to selected page
},
updateSearchTerm(term){
this.searchTerm = term;
this.isLoading = true;
this.getWorksAsync();
},
getWorksAsync: throttle(function() {
const term = this.searchTerm.toLowerCase();
callMyAPI(term)
.then(results => {
this.works = results;
this.isLoading = false;
})
}, 200)
}
}
</script>

How to get data resolved in parent component

I'm using Vue v1.0.28 and vue-resource to call my API and get the resource data. So I have a parent component, called Role, which has a child InputOptions. It has a foreach that iterates over the roles.
The big picture of all this is a list of items that can be selected, so the API can return items that are selected beforehand because the user saved/selected them time ago. The point is I can't fill selectedOptions of InputOptions. How could I get that information from parent component? Is that the way to do it, right?
I pasted here a chunk of my code, to try to show better picture of my problem:
role.vue
<template>
<div class="option-blocks">
<input-options
:options="roles"
:selected-options="selected"
:label-key-name.once="'name'"
:on-update="onUpdate"
v-ref:input-options
></input-options>
</div>
</template>
<script type="text/babel">
import InputOptions from 'components/input-options/default'
import Titles from 'steps/titles'
export default {
title: Titles.role,
components: { InputOptions },
methods: {
onUpdate(newSelectedOptions, oldSelectedOptions) {
this.selected = newSelectedOptions
}
},
data() {
return {
roles: [],
selected: [],
}
},
ready() {
this.$http.get('/ajax/roles').then((response) => {
this.roles = response.body
this.selected = this.roles.filter(role => role.checked)
})
}
}
</script>
InputOptions
<template>
<ul class="option-blocks centered">
<li class="option-block" :class="{ active: isSelected(option) }" v-for="option in options" #click="toggleSelect(option)">
<label>{{ option[labelKeyName] }}</label>
</li>
</ul>
</template>
<script type="text/babel">
import Props from 'components/input-options/mixins/props'
export default {
mixins: [ Props ],
computed: {
isSingleSelection() {
return 1 === this.max
}
},
methods: {
toggleSelect(option) {
//...
},
isSelected(option) {
return this.selectedOptions.includes(option)
}
},
data() {
return {}
},
ready() {
// I can't figure out how to do it
// I guess it's here where I need to get that information,
// resolved in a promise of the parent component
this.$watch('selectedOptions', this.onUpdate)
}
}
</script>
Props
export default {
props: {
options: {
required: true
},
labelKeyName: {
required: true
},
max: {},
min: {},
onUpdate: {
required: true
},
noneOptionLabel: {},
selectedOptions: {
type: Array
default: () => []
}
}
}
EDIT
I'm now getting this warning in the console:
[Vue warn]: Data field "selectedOptions" is already defined as a prop. To provide default value for a prop, use the "default" prop option; if you want to pass prop values to an instantiation call, use the "propsData" option. (found in component: <default-input-options>)
Are you using Vue.js version 2.0.3? If so, there is no ready function as specified in http://vuejs.org/api. You can do it in created hook of the component as follows:
// InputOptions component
// ...
data: function() {
return {
selectedOptions: []
}
},
created: function() {
this.$watch('selectedOptions', this.onUpdate)
}
In your InputOptions component, you have the following code:
this.$watch('selectedOptions', this.onUpdate)
But I am unable to see a onUpdate function defined in methods. Instead, it is defined in the parent component role. Can you insert a console.log("selectedOptions updated") to check if it is getting called as per your expectation? I think Vue.js expects methods to be present in the same component.
Alternatively in the above case, I think you are allowed to do this.$parent.onUpdate inside this.$watch(...) - something I have not tried but might work for you.
EDIT: some more thoughts
You may have few more issues - you are trying to observe an array - selectedOptions which is a risky strategy. Arrays don't change - they are like containers for list of objects. But the individual objects inside will change. Therefore your $watch might not trigger for selectedOptions.
Based on my experience with Vue.js till now, I have observed that array changes are registered when you add or delete an item, but not when you change a single object - something you need to verify on your own.
To work around this behaviour, you may have separate component (input-one-option) for each of your input options, in which it is easier to observe changes.
Finally, I found the bug. I wasn't binding the prop as kebab-case

Categories