React component state resets at unmounting - javascript

I need to persist step form's data as if the user clicks on other step. So during unmounting of step form component, I need to update the state to REDUX store
Issue
The state seems to be resetting to default as soon as I am accessing it while unmounting;
// local state
const [state, setState] = React.useState({
named: [{ id: uuid(), value: '', name: '', type: ' ' }], // 👈 initial state
});
React.useEffect(() => {
// update redux state before unmounting
return () => {
// 👇 sets itself to initial state ignoring what user has typed
console.log(state); // prints [{ id: uuid(), value: '', name: '', type: ' ' }]
updateStepThreeReduxState(state); // redux action
};
}, []);
Is there any way we can get an access to the state just before unmouting ??

State and updateStepThreeReduxState are dependencies of your effect:
//custom hook defined outside the component
function useIsMounted() {
const isMounted = useRef(false)
useEffect(() => {
isMounted.current = true
return () => {
isMounted.current = false
}
}, [])
return () => isMounted.current
}
// local state
const [state, setState] = React.useState({
named: [{ id: uuid(), value: '', name: '', type: ' ' }], // 👈 initial state
});
const mounted = useIsMounted()
React.useEffect(() => {
// update redux state before unmounting
return () => {
if(!mounted()){
console.log(state); // prints [{ id: uuid(), value: '', name: '', type: ' ' }]
updateStepThreeReduxState(state); // redux action
}
};
}, [state, updateStepThreeReduxState,mounted]);//<- added dependencies
Because you didn't add it to dependencies you have a state value that is a stale closure. Missing dependencies should cause a warning with the linter, create react app should have set all of this up for you so either use create-react-app or stop ignoring the warnings it produces.

Related

Re-render Component after Redux state change not working?

I am building a small project using the
react-flow-maker library. This library makes you able to create your own flow diagram with objects. A object can have it's own fields like textboxes, switches and dropdowns.
How does this library work?
The library has the following react component.
<FlowMaker
logic={{
introComponents: [],
components: [],
}}
onChange={data => localStorage.setItem('flowMakerExample', JSON.stringify(data))}
flow={JSON.parse(localStorage.getItem('flowMakerExample'))}
/>
Where the props used in this component have the following function:
logic -> Logic discribes the blocks and the inputs they have. It expects a object with the following properties for example.
let logic = {
introComponents: [
'hello-world'
]
components: [
{
name: 'hello-world'
title: 'Hello World'
inputs: [
{
name: 'options',
title: 'Options',
type: 'dropdown',
default: 'c',
options: [
{title: 'A', value: 'a'},
{title: 'B', value: 'b'},
{title: 'C', value: 'c'},
{title: 'D', value: 'd'},
{title: 'E', value: 'e'},
]
}
],
next: 'hello-world'
}
]
}
onChange -> This returns a the flow data from when a user changes something
flow -> Here you can add a flow to show when the drawing gets mounted, handy if you remove the component from the screen or when the drawing needs to be persistent.
My goal:
Create a block with a dropdown, fetch by API a list of items and put them in the dropdown as title and value
If the user changes something in the diagram, do a new fetch and update the options of the dropdown.
I've implemented a GET request that returns the following JSON list:
[
{"name":"name_0","sid":"0"},
{"name":"name_1","sid":"1"},
{"name":"name_2","sid":"2"},
{"name":"name_3","sid":"3"}
]
Logic.js this file contains the logic used in the FlowMaker component. Here I map the applications to right format for the options used in the dorpdown.
const Logic = async (applications, ..., ...) => {
return {
introComponents: [
'hello-world'
],
components: [
{
name: 'hello-world',
title: 'hello world',
tooltip: 'This is hello',
inputs: [
...
{
name: 'applicationName',
title: 'Application name',
type: 'dropdown',
options: [
...applications.map(app => (
{title: app.name, value: app.name + ' - ' + app.sid})
)
]
},
...
],
next: 'hello-world'
},
...
]
}
}
export default Logic;
drawerReducer.js my reducer where I initailize the new state for this drawer.
const initialState = {
logic: null,
data: null,
applications: [],
...
}
const drawerReducer = (state = initialState, action) => {
switch(action.type) {
case LOGIC:
return {
...state,
logic: action.payload
}
case DATA:
return {
...state,
data: action.payload
}
case APPLICATIONS:
return {
...state,
applications: action.payload
}
...
default:
return state;
}
}
export default drawerReducer;
drawerAction.js contains my actions where fetch the new applications, set the new data and logic.
...
import Logic from '../utils/Logic'
import { LOGIC, APPLICATIONS, ..., ..., DATA } from './types'
export const setLogic = (applications, ..., ...) => dispatch => {
Logic(applications, ..., ...)
.then(newLogic => dispatch({
type: LOGIC,
payload: newLogic
}))
}
export const setData = (newData) => dispatch => {
dispatch({
type: DATA,
payload: newData
})
}
export const setApplications = () => dispatch => {
ApplicationList()
.then(newApplications => dispatch({
type: APPLICATIONS,
payload: newApplications
}))
}
...
drawing.js here I've put the FlowMaker component and get everything together. You can see that I am using a useEffect hook to update the applications and then update the logic when the data prop changes.
import React, {useEffect} from 'react'
import FlowMaker from 'flowmaker'
import '../styles/flowmaker.css'
import Loader from '../utils/Loader'
import {setApplications, setData, setLogic } from '../actions/drawerAction'
import { connect } from 'react-redux'
const Drawer = ({logic, data, applications, doSetLogic, doSetData, doSetApplications}) => {
useEffect(() => {
doSetApplications() //dispatch new applications
doSetLogic(applications) //dispatch to set the new logic with the newly fetched applications
return () => {
//cleanup
}
}, [data])
return (
<div className='drawer-canvas'>
{ logic ?
<>
<ButtonGroup />
<FlowMaker
logic={logic} //intial state of the diagramoptions
onChange={newData => doSetData(newData)}
flow={data}
/>
</>
: <Loader />
}
</div>
)
}
const mapStateToProps = state => ({
logic: state.drawer.logic,
data: state.drawer.data,
applications: state.drawer.applications,
...
})
const mapDispatchToProps = {
doSetLogic: setLogic,
doSetData: setData,
doSetApplications: setApplications,
...
}
export default connect(mapStateToProps, mapDispatchToProps)(Drawer)
My problem
My problem is that when the useEffect data depenceny is hit. The diagram is not re-rendering the new applications options in my diagram as the new options while the logic state in Redux did change.
This is my logic state before a do a data onchange action. You can see that the options are a empty list.
Now I've added a new block in my diagram. That means that the data action will fire and the newData will be set as data, next the useEffect is triggered due the depenency [data] and the logic is set with the new logic, which means that applicationName dropdown must be filled with new options and that is true.
Now with a new redux logic action done I expect that the options are there, but they are not and that is weird because in the second picture you can see that the logic DOES update.
To conclude; my question is how can I re-render this component with the new set Redux state? I thougth when you are changing the redux state a re-render is automatily triggered like setState. Any thougths on this problem?
I know this is a lot of text / code / picture and sorry for that, i've just didnt had any better idea how to do it otherwise.
Since this week there is a new update of the package that I was using. This update makes it possible to re-render component items on specific data changes using a getInputs function. In the example main.js file there is a example logic on this.
{
name: 'frontend',
tooltip: 'Information about the proxy/load balancing server',
title: 'Frontend',
getInputs(info) {
const isHttpsInputs = info.inputs.https ? [
{
name: 'sslCert',
title: 'Add ssl cert',
tooltip: 'Add a ssl certificate',
type: 'switch',
default: true,
}
] : [];
return [
{
name: 'server',
title: 'Server',
type: 'text',
tooltip: 'The address of the proxy/load balancing server',
validation: domainCheck,
}, {
name: 'https',
title: 'The server traffic is https',
type: 'switch',
default: true,
},
...isHttpsInputs,
{
name: 'port',
title: 'Web server port',
type: 'number',
default: 443,
validation: portCheck,
}
]
},
next: 'backend'
},
The getInputs will check if the info.inputs.https is checked, if true then a extra field will be added (or updated) based on that.
In your useEffect, you seem to be dispatching the action which wants to use the updated state, however state updated are not immediate and are affected by closures.
You would need to split that logic into another useEffect
useEffect(() => {
doSetApplications() //dispatch new applications
}, [data]);
useEffect(() => {
if(applications) {
doSetLogic(applications) //dispatch to set the new logic with the newly fetched applications
}
}, [applications]);

useReducer - how to grab and update previous state

I have two problems when trying to update my state.
First of all, the first letter in the input is not being updated directly to the state - in the console.log I can see that the useReducer is first calling the initialState and then is dispatching the actions, so the registered input is one letter behind the actual user's input.
Could you please guide me and show me what would be the best way to update the state of my object? I think I should divide the dispatch on more keys, but I feel a little bit lost and don't really know how to dispatch it correctly.
PS of course I dispatched more actions - that work - so I hid some of them, hence the structure of initial state :)
Calling useReducer in Inputs.ts
const [store, dispatch] = useReducer(reducer, initialState);
Actions.ts
export const SET_INPUT_STATE = 'SET_INPUT_STATE';
export const setInputState = (dispatch, payload) => dispatch({ type: SET_INPUT_STATE, payload });
Store.ts
import { SET_INPUT_STATE } from './actions';
export interface StateType {
formState: Record<string, { value: string; isValid: boolean }>;
}
export const initialState: StateType = {
inputState: {
email: {
value: '',
isValid: false,
},
password: {
value: '',
isValid: false,
},
confirmedPassword: {
value: '',
isValid: false,
},
name: {
value: '',
isValid: false,
},
},
};
export const reducer = (state: StateType, action): StateType => {
const { type, payload } = action;
switch (type) {
case SET_FORM_STATE:
return {
...state,
inputState: { ...state.inputState, [payload.name]: { isValid: payload.isValid, value: payload.value } },
};
default:
return state;
}
};

Accessing the data from localstorage from another route with vuex

i am getting the data from a config panel route setting it to the localstorage with vuex , storeJS:
const state = {
message: [],
// console.log(message);
sec: 0,
// other state
};
const getters = {
message: (state) => {
// console.log(this.state.message);
return state.message;
},
sec: (state) => {
return state.sec;
},
// other getters
};
const actions = {
setMessage: ({ commit, state }, inputs) => {
commit(
'SET_MESSAGE',
inputs.map((input) => input.message)
);
return state.message;
},
setSec: ({ commit, state }, newSecVal) => {
commit('SET_TIMEOUT', newSecVal);
return state.sec;
},
// other actions
};
const mutations = {
SET_MESSAGE: (state, newValue) => {
state.message = newValue;
localStorage.setItem('message', JSON.stringify(newValue)); ----->this
},
SET_TIMEOUT: (state, newSecVal) => {
state.sec = newSecVal;
localStorage.setItem('sec', JSON.stringify(newSecVal)); --->this
},
// other mutations
};
export default {
state,
getters,
actions,
mutations,
};
Now i am having a home route where i want to display this ,how can i access that?
I am getting the data (not the localstorage but the regular state data) with Mapgetters and i am using it like that:
computed: {
...mapGetters({
message: "message",
sec: "sec"
}),
how can i tell him that if there is nothing (when a page reloads ) to automaticcally get the data from localstorage.
This is my MOunted
mounted() {
this.$store.dispatch("SET_MESSAGE");
this.$store.dispatch("SET_SEC");
},
I will suggest you use this package to keep your state and local storage in sync vuex-persistedstate.
Alternatively, you can set your state like this.
const state = {
message: localStorage.getItem('message') || [],
// console.log(message);
sec: localStorage.getItem('sec') || '',
// other state
};

How to use lifeCyle and setState in react?

this is app.js
class App extends Component {
constructor() {
super();
this.state = {
todo_lists: [
{ id: 1, name: "Hoc React" },
{ id: 2, name: "Hoc HTML" },
{ id: 3, name: "Hoc Jquery" },
{ id: 4, name: "Hoc CSS" }
],
showList : []
};
}
componentDidMount(){
let {showList, todo_lists} = this.state
this.setState({
showList : [...todo_lists]
})
console.log(showList)
}
}
when console.log(showList) on browser it return empty array like this [], clearly I assigned showList : [...todo_lists] in setState. help me
From Reactjs.org
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
https://reactjs.org/docs/react-component.html#setstate
So what you wrote will NOT happen immediately, and your console.log will NOT get the right values immediately afterwards.
Issue :
// this.setState is async method
this.setState({
showList : [...todo_lists]
});
console.log(showList) // <--- this will not give you updated value right after
you can use this.setState callback method for that, it is being called right after the state is set
this.setState({
// showList : [...todo_lists]
showList : this.state.todo_lists.map(todo => ({ ...todo}))
},() => {
console.log(this.state.showList) //<------ Check here
})
Suggestion :
constructor() {
super();
this.todos = [
{ id: 1, name: "Hoc React" },
{ id: 2, name: "Hoc HTML" },
{ id: 3, name: "Hoc Jquery" },
{ id: 4, name: "Hoc CSS" }
]
this.state = {
todo_lists: this.todos.map(todo => ({ ...todo})),
showList : this.todos.map(todo => ({...todo}))
};
}
The problem here is that React's setState is an async method, so it may delay the update in favor of perceived improvement in performance.
What you could do, is something like this (if you don't want to use the callback provided by the setState):
componentDidMount() {
const {showList, todo_lists} = this.state,
nextShowList = [...todo_lists];
this.setState({
showList: nextShowList
});
console.log(nextShowList); // executes immediatelly
}
If you want to use the callback provided by the setState method, simply use it like this:
componentDidMount() {
const {showList, todo_lists} = this.state;
this.setState({
showList: [...todo_lists]
}, () => console.log(this.state.showList)); // MAY NOT execute immediately
}
Also, as an additional tip (if I may), use the first argument as a function, or you may have problems setting your todo_list!
componentDidMount() {
this.setState(prevState => {
return {
showList: [...prevState.todo_lists]
};
}, () => console.log(this.state.showList));
}
Again, for the same reason: Since setState is async, you can't guarantee that the this.state outside of the setState is up-to-date!

When an action is dispatched, state of another reducer is removed. Why?

I use Redux in my project for first time. I have multiple reducers and and actions. When the first action is dispatched, state is changed. It looks okey. After dispatching second action, state is changed again but the previous changes are removed. I mean, when 'FETCH_COMPANY_INFORMATIONS' is dispatched companyName is changed and companyDesc set to initial value. Then 'FETCH_INITIAL_MEMBER' is dispatched and companyName is removed but companyDesc is still there and member payloads are also changed. What is my mistake? Thanks.
I tried many ways to solve this but still continue. I check this on Redux DevTools.
memberReducer
const initialState = {
username: '',
companyId: '',
isAdmin: '',
photo: '',
};
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_INITIAL_MEMBER:
return {
...state,
username: action.payload.username,
companyId: action.payload.companyId,
isAdmin: action.payload.isAdmin,
};
default:
return state;
}
};
companyReducer
const initialState = {
companyName: 'companyName',
companyDesc: 'companyDesc',
};
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_COMPANY_INFORMATIONS:
return {
...state,
companyName: action.payload.companyName,
};
default:
return state;
}
};
memberAction
const fetchInıtıalMember = async muuid => {
axios
.get(`/api/member/${muuid}`)
.then(response => {
const username = response.data.mname;
const isAdmin = response.data.misAdmin;
const companyId = response.data.cid;
store.dispatch({
type: FETCH_INITIAL_MEMBER,
payload: {
username,
isAdmin,
companyId,
},
});
})
.catch(error => {});
};
companyAction
const fetchCompanyInformations = () => {
store.dispatch({
type: FETCH_COMPANY_INFORMATIONS,
payload: { companyName: 'dispacthedCompanyName' },
});
};
Edit:
The code above is correct. My mistake is about importing the constants. This Redux implementation works well. I was storing all action type constant in a types.js file. I import this type constants in the another files wrongly. After changing it my problem is solved.

Categories