I am building 3 diffent modes/radio buttons with ant design react and I will like to get the previous state from the local-storage and assign it to my value.
It works only when I hard code the value, there must be a cleaner way to achieve this.
class Settings extends React.Component {
state = {
mode: 'Something Mode 1',
}
componentDidMount() {
chrome.storage.local.get('AllModes', (result) => {
this.setState({ mode: result.AllModes})
})
}
handleMode () {
let currentMode = localStorage.getItem(['AllModes']);
console.log(currentMode)
this.setState(( currentMode ) => {
return {
mode: currentMode.mode
}
})
}
<RadioGroup onChange={this.onChange}
className="radio__group"
defaultValue={this.handleMode}>
</RadioGroup>
}
There are several ways to do this, but I'll describe a simple one.
You can persist the component's state in localStorage as a JSON-encoded string and restore it when the component is recreated.
We can define our save and loading methods:
function getStoredComponentState(initialState) {
const storedState = localStorage.getItem(checkedStorageKey);
return storedState ? JSON.parse(storedState) : initialState;
}
function storeComponentState(state) {
localStorage.setItem(checkedStorageKey, JSON.stringify(state))
}
We initialize our state from what was stored, providing a default in case there is no stored previous state:
constructor() {
super();
this.state = getStoredComponentState({
checked: false,
})
}
We persist any changes made to storage when they happen:
setState(state) {
super.setState(state);
storeComponentState(state);
}
And it works! You can make a change, refresh the page, and see that the previous state is restored.
JSFiddle
Related
When you open reactjs.org, under "Declarative" header, there is a sentence: React will efficiently update and render just the right components when your data changes.
For a couple of my apps, I'm using the following structure:
App
| AppContainer(all the app logic, protected before login)
| Login(Login form)
This structure works well if you return 2 different components inside App's render, according to the user's credentials.
render(){
if(isUserLoggedIn()){
return <AppContainer />;
}
return <Login />;
}
Inside the Login component, I'm refreshing the page with window.location.reload so the App's render will be triggered, and I'll get the AppContainer component.
But it feels a little like jQuery + Angular. Is there a better (more React) way to trigger render function, or is this how things should be?
Is there a better(more React) way to trigger render function...
The usual way is to have state, in this case at minimum a boolean for whether the user is logged in, and update that state when the user logs in successfully or logs out. Updating state triggers rendering.
In your case, since you're using Redux, you'd probably have your state there.
I don't use Redux (yet?), this is vaguely what it would look like without, roughly (if you're using a class component as you seem to be):
class App extends Component {
constructor(props) {
super(props);
this.state = {
loggedIn: /*...initial value, perhaps from web storage or cookie...*/;
};
this.onLogin = this.onLogin.bind(this);
this.onLogout = this.onLogout.bind(this);
}
onLogin() {
// ...probably stuff here, then:
this.setState({loggedIn: true});
}
onLogout() {
// ...probably stuff here, then:
this.setState({loggedIn: false});
}
render() {
if (this.state.logedIn) {
return <AppComponent onLogout={this.onLogout}/>;
}
return <Login onLogin={this.onLogin}/>;
}
}
or with hooks:
const App = () => {
const [loggedIn, setLoggedIn] = useState(/*...initial value, perhaps from web storage or cookie...*/);
const onLogin = useCallback(() => {
// ...probably stuff here, then:
setLoggedIn(true);
}, [loggedIn]);
const onLogout = useCallback(() => {
// ...probably stuff here, then:
setLoggedIn(false);
}, [loggedIn]);
if (this.state.logedIn) {
return <AppComponent onLogout={onLogout}/>;
}
return <Login onLogin={onLogin}/>;
}
(again, roughly)
If you need to update the component state, then you can pass an observable and listen for changes or use some state management library.
Here is one possible solution:
Create observable class
declare type IObserverHandler = (event: any) => void;
export class Observable {
private observers: IObserverHandler[] = [];
public subscribe(observer: IObserverHandler) {
if (!this.observers.includes(observer)) {
this.observers.push(observer);
}
}
public unsubscribe(observer: IObserverHandler) {
this.observers = this.observers.filter(o => o !== observer);
}
public publish(event: any) {
for (const observer of this.observers) {
observer(event);
}
}
}
Create Login class that will publish events on actions such as login or logout
class Login extends Observable {
public login() {
this.publish({ value: true });
}
public logout() {
this.publish({ value: false });
}
}
In component subscribe to observer and update component state using event value
export abstract class Component extends React.Component<any, any> {
private observer: IObserverHandler;
private observable: Login;
constructor(props: any) {
super(props);
this.observable = this.props.observable;
this.state = { isAuthenticated: false }
this.observer = (event) => {
this.setState({ isAuthenticated: event.value })
}
this.observable.subscribe(this.observer);
}
componentWillUnmount() {
this.observable.unsubscribe(this.observer);
}
}
You can use useNavigate and navigate to the same url you are on. For example, instead of window.location.reload(), you can say navigate("/...your current url....")
window.location.reload() is not the best option everytime. It works on localhost, but for example on when you deploy it to the internet by using services such as "Netlify", it can can cause "not found url" error
Creating some extra state and tracking them for re-rendering your page might unnecessarily complicate your code.
Whenever setState() is called, the component doesn't seem to rerender. As you can see by my comments, the state does in fact change and render seems to be called again, but if I don't add that if statement and simply add a paragraph tag that displays the data it will give me an error. I'm sure I'm missing something simple, but any help is appreciated.
import React from "react";
import axios from "axios";
import { constants } from "../constants/constants";
const { baseURL, apiKey, userName } = constants;
class User extends React.Component {
constructor(props) {
super(props);
this.state = {
user: []
};
}
componentDidMount() {
let getUserInfo = axios.create({
baseURL,
url: `?
method=user.getinfo&user=${userName}&api_key=${apiKey}&format=json`
});
getUserInfo().then(response => {
let data = response.data;
console.log(data.user.playcount); //logs second, displays correct
this.setState(state => ({
user: data
}));
});
}
render() {
console.log(this.state); //logs first and third, doesn't work on first but does on third
let toReturn;
if (this.state.user.length > 0) {
toReturn = <p>{this.state.user.user.playcount}</p>;
} else {
toReturn = <p>didn't work</p>;
}
return <div>{toReturn}</div>;
}
}
export default User;
React LifeCycle function sequence is Constructor and then it calls render method.
In constructor method it initialises the state which is currently empty user array.
Now it calls render() method as this.state.user is an empty array, referencing something out of it gives an error
this.state.user.user.playcount
this will generate an error if you dont have if condition.
After the first render it will call componentDidMount, now you fetch something update state. As setState occurred, render will be called again Now you have something in this.state.user then displaying will happen.
this.state.user.length > 0 is true
Look at this: https://reactjs.org/docs/react-component.html and https://reactjs.org/docs/conditional-rendering.html
You can right in single tag using conditional render like this
<p>{this.state.user.length ? this.state.user.user.playcount : 'loading'}
Hope this helps.
I think your problem might have something to do with the changing shape of the user value. You initialise the value to an empty array, but then—after the fetch is done—you assume it's an object (by using user.user).
Maybe you could simplify the code a bit to look more like the one below?
/* imports */
class User extends React.Component {
constructor(props) {
super(props);
this.state = {
user: null // Make it explicit there's no value at the beginning.
};
}
componentDidMount() {
let getUserInfo = axios.create(/* ... */);
getUserInfo().then(response => {
let data = response.data;
this.setState({ // No need to for a setter function as you dno't rely on the previous state's value.
user: data.user // Assign the user object as the new value.
});
});
}
render() {
let toReturn;
// Since it's now a `null`, you can use a simple existence check.
if (this.state.user) {
// User is now an object, so you can safely refer to its properties.
toReturn = <p>{this.state.user.playcount}</p>;
} else {
toReturn = <p>No data yet.</p>;
}
return <div>{toReturn}</div>;
}
}
export default User;
My app has a user control panel and when the page is loaded it fetch data from the server using Redux.
In construction the component create an initial state like:
const { profile } = this.props;
this.state = {
prop1: profile.prop1 || '',
prop2: profile.prop2 || '',
nested: {
nestedProp1: profile.nested.nestedProp1 || '',
}
...etc...
}
On componentWillMount I have this:
componentWillMount() {
const { user, getProfile } = this.props;
if (user.profile_id) {
getProfile(user.profile_id);
}
}
What I don't understand are 2 things:
Is the approach correct? I'm using state to handle form inputs.
How can I update the state when fetched? There are plenty of properties in this profile object and I was wondering to update all the states in a very simple way, and not one by one...
1.If you are using redux,I think there is no need to use state to manage date, instead you can use props(redux) to handle all the date in your project.
Then,if you want to update the date, you should create action to update the globally unique date that stored in redux.
2.About how to handle the input, when the user have input value, you can create an action, create a copy with the initial state then update state with your input action.
function updateInput(state = initialState, action) {
switch (action.type) {
case 'INPUT':
return Object.assign({}, state, {
profile_id: action.profile
})
return state;
}
}
You could use static getDerivedStateFromProps(props, state) -method to update your component state from props whenever your Redux store updates. It has two params props and state.
class App extends React.Component {
// https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
static getDerivedStateFromProps(props, state){
// Compare and update state only if props differs from state
if(JSON.stringify(state) !== JSON.stringify(props.profile)){
return { ...props.profile }
}
// If it doesn't differ do not update state
return null
}
// Do an state initialization
state = { ...this.props.profile }
// Prefer componentDidMount -for doing fetch and updating component state
componentDidMount(){
const { user, getProfile } = this.props;
if (user.profile_id) {
getProfile(user.profile_id);
}
}
render(){
return (
<div className="App">
{/** Render content */}
</div>
);
}
}
Rest spread operator, what is used to fill up state is ES6 syntax.
If you use Babel you might need to add rest spread operator -plugin to your .babelrc -config. https://babeljs.io/docs/en/babel-plugin-proposal-object-rest-spread
I am unable to get props inside constructor that I have implemented using redux concept.
Code for container component
class UpdateItem extends Component{
constructor(props) {
super(props);
console.log(this.props.item.itemTitle) // output: undefined
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
//If the input fields were directly within this
//this component, we could use this.refs.[FIELD].value
//Instead, we want to save the data for when the form is submitted
let state = {};
state[e.target.name] = e.target.value.trim();
this.setState(state);
}
handleSubmit(e) {
//we don't want the form to submit, so we pritem the default behavior
e.preventDefault();
let errors = {};
errors = this._validate();
if(Object.keys(errors).length != 0) {
this.setState({
errors: errors
});
return;
}
let itemData = new FormData();
itemData.append('itemTitle',this.state.itemTitle)
this.props.onSubmit(itemData);
}
componentDidMount(){
this.props.getItemByID();
}
componentWillReceiveProps(nextProps){
if (this.props.item.itemID != nextProps.item.itemID){
//Necessary to populate form when existing item is loaded directly.
this.props.getItemByID();
}
}
render(){
let {item} = this.props;
return(
<UpdateItemForm
itemTitle={this.state.itemTitle}
errors={this.state.errors}
/>
);
}
}
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
function mapStateToProps(state, ownProps){
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
function mapDispatchToProps (dispatch, ownProps) {
return {
getItemByID:()=>dispatch(loadItemByID(ownProps.params.id)),
onSubmit: (values) => dispatch(updateItem(values))
}
}
export default connect(mapStateToProps,mapDispatchToProps)(UpdateItem);
Inside render() method am able to get the props i.e. item from the redux but not inside constructor.
And code for the actions to see if the redux implementation correct or not,
export function loadItemByID(ID){
return function(dispatch){
return itemAPI.getItemByID(ID).then(item => {
dispatch(loadItemByIDSuccess(item));
}).catch(error => {
throw(error);
});
};
}
export function loadItemByIDSuccess(item){
return {type: types.LOAD_ITEM_BY_ID_SUCCESS, item}
}
Finally my reducer looks as follows,
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
I have googled to get answers with no luck, I don't know where i made a mistake. If some one point out for me it would be a great help. Thanks in advance.
The reason you can't access the props in the constructor is that it is only called once, before the component is first mounted.
The action to load the item is called in the componentWillMount function, which occurs after the constructor is called.
It appears like you are trying to set a default value in the mapStateToProps function but aren't using it at all
function mapStateToProps(state, ownProps){
// this is never used
let item = {
itemTitle: ''
};
return {
item: state.itemReducer
};
}
The next part I notice is that your are taking the state from redux and trying to inject it into the component's local state
this.state = {
itemTitle: this.props.item.itemTitle,
errors: {}
};
Mixing redux state and component state is very rarely a good idea and should try to be avoided. It can lead to inconsistency and and hard to find bugs.
In this case, I don't see any reason you can't replace all the uses of this.state.itemTitle with this.props.items.itemTitle and remove it completely from the component state.
Observations
There are some peculiar things about your code that make it very difficult for me to infer the intention behind the code.
Firstly the reducer
export default function itemReducer(state = initialState.item, action) {
switch (action.type) {
case types.LOAD_ITEM_BY_ID_SUCCESS:
return Object.assign([], state = action.item, {
item: action.item
});
default:
return state;
}
}
You haven't shown the initialState object, but generally it represents the whole initial state for the reducer, so using initialState.item stands out to me. You may be reusing a shared initial state object for all of the reducers so I'm not too concerned about this.
What is very confusing the Object.assign call. I'm not sure it the intention is to output an object replacing item in the state, or if it is to append action.item to an array, or to have an array with a single item as the resulting state. The state = action.item part is also particularly puzzling as to it's intention in the operation.
This is further confused by the PropTypes for UpdateItem which requires item to be an array
UpdateItem.propTypes = {
item: PropTypes.array.isRequired
};
But the usage in the component treats it like and object
this.state = {
// expected some kind of array lookup here |
// V---------------
itemTitle: this.props.item.itemTitle,
errors: {}
};
Update from comments
Here is a example of what I was talking about in the comments. It's a simplified version of your code (I don't have all your components. I've also modified a few things to match my personal style, but hopefully you can still see what's going on.
Everything works fine until I run a method to add some objects to the store, that is triggering a re render on the view... The thing is in the first page load the store on the component is a map with all its methods, after update and when the component gets re rendered it loses the methods and is turned into a plain js object, obviously the component tries to do a store.get() for example and it fails :S
here it is an screenshot:
and some code:
class BlogStore {
constructor() {
this.bindListeners({
handleBlogs: Actions.UPDATE_BLOGS,
handleBlogPost: Actions.UPDATE_BLOG_POST,
addMoreBlogs: Actions.ADD_MORE_BLOGS,
});
console.log('STORE: state before initialization', this.store);
/* here is where the state structure is defined */
this.state = Immutable.Map({
blogs: {
blogList: [],
meta: {
count: 0,
},
currentPage: 1,
},
blogPost: []
});
}
/* here is where the state gets modified */
addMoreBlogs(blogs) {
console.log('addMoreBlogs executed');
console.log('STORE: state before convertion to js', this.state);
console.log('we have to add this to the store', blogs);
//const currentPage = this.state.blogs.currentPage + 1;
console.log('STORE: blogList should not be empty', this.state.get('blogs').blogList.concat(blogs.blogList));
const updatedBlogs = this.state.get('blogs').blogList.concat(blogs.blogList);
const updatedMeta = this.state.get('blogs').meta;
console.log('STORE: updatedBlogs', updatedBlogs);
console.log('STORE, updatedMeta', updatedMeta);
this.setState(this.state.setIn(
['blogs'],
{
blogList: updatedBlogs,
meta: updatedMeta,
currentPage: 1,
}
));
console.log('STORE: updated state', this.state);
}
...
and the component:
class BlogsWrapper extends React.Component {
constructor(props) {
super(props);
/* get the store data and transform it to plain js */
this.state = Store.getState();
//console.log('BLOGSWRAPPER: state gotten in constructor:', this.state);
this._onChange = this._onChange.bind(this);
}
componentDidMount() {
Store.listen(this._onChange);
// if (this.state.get('blogs').isEmpty()) {
// this.context.router.push('/blog/not-found');
// return;
// }
}
componentWillUnmount() {
Store.unlisten(this._onChange);
//console.log('BLOGSWRAPPER: resetting page counter');
}
_onChange() {
//this.setState(Store.getState().toJS());
this.setState(Store.getState());
console.log('BLOGSWRAPPER: onchange fired', this.state);
}**
...
any help on this? I can't figure out what is going on, I tried also re create the store object on the addMoreBlogs method with the same result. Any help/suggestion will be appreciated. Thanks
I also had the same issue recently, I think you should use mergeIn instead of setIn if planning to modify state with an object.
Check this jsbin for the same explanation.
Can you try this:
this.setState(this.state.setIn(
['blogs'],
Immutable.formJS({
blogList: updatedBlogs,
meta: updatedMeta,
currentPage: 1,
})));
Obviously, this is a hack. When you work with immutable state, you should avoid updating all state in that manner. Instead, you need to update exact field of you Immutable object. That's why guys from Facebook recommend keeping state as flat as possible.