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.
Related
I'm learning Redux and Redux Toolkit, but I don't understand why autocomplete doesn't work when I'm trying to dispatch an action (see the image below).
I imported the "action" but WebStorm can't see any methods.
On VSCode it works very well.
Here the action :
import {createSlice} from "#reduxjs/toolkit";
const initialCounterState = { counter: 0, showCounter: true };
const counterSlice = createSlice({
name: "counter",
initialState: initialCounterState,
reducers: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
increase(state, action) {
state.counter += action.payload;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
},
});
export const counterActions = counterSlice.actions;
export default counterSlice.reducer
Like you can see above , the first image is WebStorm , the second is vscode.
Vscode detects all the methods , WebStorm doesn't , I didn't find any issue like these on google..
I'm wondering if it's simply normal to not see theses methods on WebStorm , it would be weird , WebStorm it's powerful usually..
I just found the solution, experimenting with different ways or rearranging my files. I'm taking the same tutorial with the same profesor at Udemy. Is just a matter or organizing your files and imports/exports in a specific way.
Instead of exporting each SliceAction directly from its respective slice file, each one of them must be centralized on the store index file and exported from there.
Solution: Code example:
File: src/store/counterSlice.js
import {createSlice} from '#reduxjs/toolkit';
const counterInitialState = {
counter: 0,
showCounter: true,
};
const counterSlice = createSlice({
name: 'counterSlice',
initialState: counterInitialState,
reducers: {
incrementCounter(state) {
state.counter++;
},
decrementCounter(state) {
state.counter--;
},
increaseByCounter(state, action) {
state.counter = state.counter + action.payload.amount;
},
decreaseByCounter(state, action) {
state.counter = state.counter - action.payload.amount;
},
toggleCounter(state) {
state.showCounter = !state.showCounter;
},
}
});
export default counterSlice;
File: src/store/authSlice.js
import {createSlice} from '#reduxjs/toolkit';
const authInitialState = {
isAuthenticated: false,
};
const authSlice = createSlice({
name: 'authSlice',
initialState: authInitialState,
reducers: {
logIn(state) {
state.isAuthenticated = true;
},
logOut(state) {
state.isAuthenticated = false;
},
toggleLogging(state) {
state.isAuthenticated = !state.isAuthenticated;
},
}
});
export default authSlice;
File: src/store/index.js
import {configureStore} from '#reduxjs/toolkit';
import counterSlice from "./counterSlice";
import authSlice from "./authSlice";
const store = configureStore({
reducer: {
counterSliceReducer: counterSlice.reducer,
authSliceReducer: authSlice.reducer,
},
});
export const counterSliceActions = counterSlice.actions;
export const authSliceActions = authSlice.actions;
export default store;
After organizing your files this way, you will see that now you have a perfect visibility over the action creator methods in your imported CaseReducerActions object (like authSliceActions or counterSliceActions, for example).
So this is how my WebStorm IDE looks like right now:
File: src/App.js
File: src/components/Counter/Counter.jsx
As you can see, now I have auto completion (autocomplete feature) using WebStorm.
Even if you are not using TypeScript directly and only write JavaScript, your editor will still use a library's TypeScript typings to give you things like autocomplete. So even if you are not directly using TypeScript yourself, this is still relevant to you.
This is a known issue, but it is a bit broader:
Unfortunately, WebStorm does not work very well with TypeScript and where other editors just use the library's TypeScript typings (even in JavaScript scenarios) for autocomplete and error messages, WebStorm just randomly guesses wrong stuff because things have kinda similar names.
For Redux Toolkit specifically, there are multiple issues open in their bug tracker and they do a great job at ignoring that: https://youtrack.jetbrains.com/issue/WEB-46527 and https://youtrack.jetbrains.com/issue/WEB-42559 for example.
So yes, due to bugs in WebStorm it is unfortunately "normal to not see theses methods on WebStorm" until the bugs I linked above are fixed.
At this point I can only recommend not using WebStorm for any kind of software development involving JavaScript or TypeScript.
Visual Studio Code might need a few extensions hand-picked and installed to get on feature parity with WebStorm, but it is free and works very well, so I cannot recommend that enough.
What ways to change language in React can you suggest without using external libraries? My way is to use the ternary operator {language === 'en'? 'title': 'titre'}. If language is en, displaytitle if not, display titre. What other way can you recommend. For example, that the translations should be placed in a separate json file.
Code here: https://stackblitz.com/edit/react-eu9myn
class App extends Component {
constructor() {
super();
this.state = {
language: 'en'
};
}
changeLanguage = () => {
this.setState({
language: 'fr'
})
}
render() {
const {language} = this.state;
return (
<div>
<p>
{language === 'en' ? 'title' : 'titre'}
</p>
<button onClick={this.changeLanguage}>change language</button>
</div>
);
}
}
Internationalization (i18n) is a hard problem with a few existing, standard solutions designed by expert translators and linguists to account for the breadth of language quirks across the world. You shouldn't generally try to come up with your own solution, even when you are fluent in all target languages.
That doesn't mean you need a library (you could implement one of those standards yourself) but writing the i18n logic inline will not scale and probably won't work well.
The easiest case of i18n is if you're translating strings that do not depend on context and are complete sentences with no interpolation. You could get away with a very basic approach there, like using a big dictionary of translations and just looking up each string in it. It would look sort of like your ternary but at least it would scale for many languages, and it would be reasonable to do that with no library:
l10n = {
'title': {en: 'title', fr: 'titre'}
}
<p>
{l10n['title'][lang]}
</p>
However, if there is going to be string interpolation in your website/application/whatever, please consider a library that implements, say, ICU.
Now, let me show you why it would be a bad idea. Suppose you have the string "you can see (n) rocks" where you want to replace (n) with an actual number, and you want the sentence to be grammatically correct so you need to compute number agreement, right ? so, "0 rocks", "1 rock", "2+ rocks"… looks like English plural is just adding an "s" (not true, but let's assume for now), you could implement that with ternaries. I see you used French in your example so, how about that ? "0 cailloux", "1 caillou", "2+ cailloux". Right, there are multiple plural forms in French. How do you write your code to account for that ? And what if you need a German translation ? maybe the translator will decide that the object should go first in the sentence, rather than last. How does your code handle word order based on language ?
All these problems should be delegated to the translator who encodes them into an ICU string, which is then evaluated by some code given a context to get a correct translation. Whether you use a library or implement it yourself, what you want in the end is some function — let's call it localize(string, context) that is pretty much independent from React and that you use in your components like this:
import localize from './somewhere'
<p>
{localize('title')}
</p>
If you really want to, you can pass the locale as an argument and have it stored in React's state somehow. This library decided it wasn't necessary because real users rarely switch language and it's OK to reload the whole application when that happens.
I just implemented a simple language component for work that uses a Localisation context/provider and a dictionary (e.g JSON). I'll go through the steps, and there's a workable codesandbox example at the end. This is a very basic approach, but it works well for us at the moment.
The example has:
1) A simple "dictionary" that contains the tokens you want to translate in each language defined by a short code
{ EN: { welcome: 'Welcome' }, FR: { welcome: 'Bienvenue' }, IT: { welcome: 'Benvenuto' } };
2) An initial state and reducer that you can update when the language changes
export const initialState = {
defaultLanguage: 'EN',
selectedLanguage: 'IT'
}
export function reducer(state, action) {
const { type, payload } = action;
switch (type) {
case 'LANGUAGE_UPDATE': {
return { ...state, selectedLanguage: payload };
}
default: return state;
}
}
3) A Localisation Context/Provider. You can wrap your code in the provider and every child component can get access to the state through the context. We import the dictionary and state/reducer, create the new context and then set up the provider into which we pass the state and dictionary.
import dictionary from './dictionary';
import { initialState, reducer } from './localisationReducer';
export const LocalisationContext = React.createContext();
export function LocalisationProvider({ children }) {
const localisationStore = useReducer(reducer, initialState);
return (
<LocalisationContext.Provider value={{ localisationStore, dictionary }}>
{children}
</LocalisationContext.Provider>
);
}
4) An example app. You can see the LocalisationProvider wrapping the other elements, but also a dropdown, and a component called Translate. I'll describe those next.
<LocalisationProvider>
<Dropdown />
<div className="App">
<h1>
<Translate token="welcome" />
</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
</LocalisationProvider>
5) The dropdown accesses the Localisation context and builds a dropdown with the languages. The key part is the handleSelected function which uses the dispatch from the localisation store to change the state (update the language):
import { LocalisationContext } from './localisation';
const langs = [
{ shortCode: 'EN', label: 'English' },
{ shortCode: 'FR', label: 'Français' },
{ shortCode: 'IT', label: 'Italiano' }
];
export function Dropdown() {
const {
localisationStore: [ state, dispatch ]
} = useContext(LocalisationContext);
const { selectedLanguage } = state;
const handleSelected = (e) => {
const { target: { value } } = e;
dispatch({ type: 'LANGUAGE_UPDATE', payload: value });
}
function getOptions(langs, selectedLanguage) {
return langs.map(({ shortCode, label }) => {
return <option value={shortCode}>{label}</option>
});
}
return (
<select onChange={handleSelected}>
{getOptions(langs, selectedLanguage)}
</select>
);
}
6) The Translate component which also accesses the state and dictionary through the context, and performs the translation based on the selected language.
import { LocalisationContext } from './localisation';
export default function Translator({ token }) {
const {
localisationStore: [state], dictionary
} = useContext(LocalisationContext);
const {
selectedLanguage, defaultLanguage
} = state;
const translatedToken = dictionary[selectedLanguage][token] || dictionary[defaultLanguage][token];
return (
<Fragment>
{translatedToken}
</Fragment>
);
}
Here's the codesandbox example for you to explore. Just select a new language from the dropdown to see the main "welcome" text change.
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>
I'm trying to create a Quill.js editor instance once component is loaded using mounted() hook. However, I need to set the Quill's content using Quill.setContents() on the same mounted() hook with the data I received from vuex.store.state .
My trouble here is that the component returns empty value for the state data whenever I try to access it, irrespective of being on mounted() or created() hooks. I have tried with getters and computed properties too. Nothing seems to work.
I have included my entry.js file, concatenated all the components to make things simpler for you to help me.
Vue.component('test', {
template:
`
<div>
<ul>
<li v-for="note in this.$store.state.notes">
{{ note.title }}
</li>
</ul>
{{ localnote }}
<div id="testDiv"></div>
</div>
`,
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
}
},
created() {
this.$store.dispatch('fetchNotes')
},
mounted() {
// Dispatch action from store
var quill = new Quill('#testDiv', {
theme: 'snow'
});
// quill.setContents(JSON.parse(this.localnote.body));
},
methods: {
setLocalCurrentNote(note) {
console.log(note.title)
return this.note = note;
}
}
});
const store = new Vuex.Store({
state: {
message: "",
notes: [],
currentNote: {}
},
mutations: {
setNotes(state,data) {
state.notes = data;
// state.currentNote = state.notes[1];
},
setCurrentNote(state,note) {
state.currentNote = note;
}
},
actions: {
fetchNotes(context) {
axios.get('http://localhost/centaur/public/api/notes?notebook_id=1')
.then( function(res) {
context.commit('setNotes', res.data);
context.commit('setCurrentNote', res.data[0]);
});
}
},
getters: {
getCurrentNote(state) {
return state.currentNote;
}
}
});
const app = new Vue({
store
}).$mount('#app');
And here is the index.html file where I'm rendering the component:
<div id="app">
<h1>Test</h1>
<test :localnote="$store.state.currentNote"></test>
</div>
Btw, I have tried the props option as last resort. However, it didn't help me in anyway. Sorry if this question is too long. Thank you for taking your time to read this. Have a nice day ;)
I copied your code and tested it ( of-course I created my own dummy notes so I could remove the get request ) and I was able to get the notes display on a page.
A couple of things that I realized from your code, you may need to add a store property as there are places in your component ( test ) where you are referencing it, yet you only define it on the 'app' component. So in this section of your code modify as shown below:
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
store : store
}
},
The key difference is the definition of the 'store' property. Please note that, what you have done, defining a "store" property in your app component, is correct, but the very same needs to be defined in "test" component as I have shown in the above code snippet above.
Second thing is, you are using $store and I guess that gives you undefined, unless as you said, in the libraries that you included this resolves accordingly, but on my side I had to remove all references of "$store" and replace it with just "store" (without the dollar sign).
Lastly for testing purposes, I would advise you to also
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.