I have the following state class:
import { observable, action } from 'mobx';
import axios from 'axios';
export default class AppState {
#observable user;
#observable pToken;
constructor() {
this.user = {};
this.pToken = localStorage.getItem('pToken');
}
async fetchData(query) {
const body = JSON.stringify({ query, });
const response = await axios.post('url', body, {
headers: {
'Content-Type': 'application/json',
token: localStorage.getItem('pToken')
}
});
const user = response.data.data.user;
console.log('Got user', user);
this.setUser(user);
}
#action setUser(user) {
this.user = user;
}
}
and in my component:
#inject('store')
#observer
export default class Header extends Component {
constructor(props) {
super(props);
this.store = this.props.store.appState;
}
render() {
const { user } = this.store;
console.log('store', this.store);
return (
<header className='header'>
User - {user.username}
</header>
);
}
}
Unfortunately, the state user property returns a Proxy object, while the user log shows the user object. Any idea what I'm missing?
Everything is working as intended, MobX v5 relies on proxies under the hood so when you log observable objects it usually shows some internal implementation.
You can use toJS MobX method console.log(toJS(user)) or just destructure user object console.log({ ...user })
toJS docs: https://mobx.js.org/refguide/tojson.html
Related
first questioner here!
I'm new to React and find it confusing to manage state with redux. From the redux-logger output, it seems that I am successfully changing the redux state regarding a user sign-in but I don't really know how to set it to props, and as such, I'm getting an undefined value for currentUser (which is the prop I want to manage across all my pages). I'm using both withRouter and Redux in an effort to pass user properties to app.js.
It starts with an API call to the backend to see if the user can login, if success then returns an object {isAdmin: "", uId: ""}.
import React from "react";
import { withRouter } from "react-router-dom";
import { setCurrentUser } from "../../redux/user/user-actions";
import { connect } from "react-redux";
// sign-in.jsx
class Login extends React.Component {
constructor(props) {
super(props);
}
onSubmitClick = async (e) => {
e.preventDefault();
fetch("/api/login", {
method: "post",
body: JSON.stringify({
email: "",
password: "",
}),
})
.then((res) => res.json())
.then((user) => {
if (user.error) {
this.setState({ error: user.error });
} else {
// Set the user in redux too:
this.props.dispatch(setCurrentUser(user));
// Redirect to main page after login
this.props.history.push({
pathname: "/",
search: "?uid=" + user.key + "?admin=" + user.admin,
state: { userId: user.key, isAdmin: user.admin },
});
}
});
};
render() {
return (...)
}
const mapStateToProps = ({ user }) => ({
currentUser: user.currentUser,
});
export default connect(mapStateToProps)(withRouter(Login));
The line with code: this.props.dispatch(setCurrentUser(user)); successfully changed the state but not the props value.
Here is the redux stuff:
// user-actions.js --------------------------------------------------------------------------------------
export const setCurrentUser = (user) => ({
type: "SET_CURRENT_USER",
payload: user,
});
// user-reducer.js --------------------------------------------------------------------------------------
// The initial state is basically a null user (ID)
const initialState = {
user: null,
};
/*
This is essentially a function that takes the current state
and action as an argument and returns a new state result.
i.e. (state, action) => newState
*/
const userReducer = (state = initialState, action) => {
// Conditional for the current action type
if (action.type.localeCompare("SET_CURRENT_USER") === 0) {
// Return a new state object
return {
// Which has the existing data but also..
...state,
// The new user object (just an ID at this point)
user: action.payload,
};
} else {
// Otherwise we return the state unchanged
// (usually when the reducer doesnt pick up the certain action)
return state;
}
};
export default userReducer;
// store.js --------------------------------------------------------------------------------------
import { createStore, applyMiddleware } from "redux";
/*
Useful for debugging redux --> logger
Is a logger middleware that console.logs the actions fired and change of state
*/
import logger from "redux-logger";
import rootReducer from "./root-reducer";
const middlewares = [logger];
const store = createStore(rootReducer, applyMiddleware(...middlewares));
export default store;
// root-reducer.js --------------------------------------------------------------------------------------
import { combineReducers } from "redux";
import userReducer from "./user/user-reducer";
export default combineReducers({
user: userReducer,
});
And finally, the App.js relevant code
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
...props,
u_id: null,
};
}
unsubscribeFromAuth = null;
componentDidMount() {
const { setCurrentUser } = this.props;[enter image description here][1]
const userState = this.props.location;
console.log(this.props);
// Make sure that state for a user isnt undefined
if (userState.state) {
this.unsubscribeFromAuth = true;
const user = userState.state.userId;
this.props.dispatch(setCurrentUser(user));
}
console.log(this.props);
}
componentWillUnmount() {
this.unsubscribeFromAuth = false;
}
render() {
return (...)
}
}
const mapStateToProps = (state) => ({
currentUser: state.currentUser,
});
//Access the state and dispatch function from our store
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
dispatch,
});
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
Console output with redux-logger:
https://i.stack.imgur.com/r9JyV.png
As you can see, currentUser is undefined but all props in the location are there, I'm probably making some really dumb mistake when setting currentUser with the setCurrentUser action, both in the login and then again in the componentDidMount in the app.jsx
I'll add more detail upon request
Any help would be appreciated GREATLY! :)
You are saving the user in redux under user but you are trying to access it in the mapStateToPRops via currentUser:
const mapStateToProps = (state) => ({ currentUser: state.currentUser, });
Change it to const mapStateToProps = (state) => ({ currentUser: state.user, });
and it should work.
Also this:
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
dispatch,
});
is equivalente to:
const mapDispatchToProps = ({
setCurrentUser
});
https://react-redux.js.org/using-react-redux/connect-mapdispatch#defining-mapdispatchtoprops-as-an-object
I have a lower order page component that needs to fetch data in the getInitialProps(). It successfully fetches the data, but it does return as prop in the component.
Below is the code I'm working on
import React, { Component } from 'react';
import DefaultPage from '../hocs/DefaultPage';
import PageLayout from '../components/PageLayout';
import { getJwtFromLocalCookie, getJwtFromServerCookie } from '../utils/auth';
import { getUser } from '../services/users';
class Profile extends Component {
static async getInitialProps(ctx) {
let user;
const jwt = process.browser ? getJwtFromLocalCookie() : getJwtFromServerCookie(ctx.req);
try {
const {data} = await getUser(ctx.query.id, jwt);
user = data;
}
catch (err) {
if(err.code === 404)
ctx.res.statusCode = 404;
}
console.log(user);
return { user };
}
constructor(props) {
super(props);
this.state = { user: null };
}
render() {
console.log(this.props);
return (
<PageLayout
active=""
loggedUser={this.props.loggedUser}
>
</PageLayout>
);
}
}
export default DefaultPage(Profile);
The console.log() in the getInitialProps() does display the correct data, but the console.log() in render() doesn't have the user prop.
Ok, I found the solution, turns out in the getInitialProps() of the higher order component the getInitialProps() of the lower order component was returning a promise and needed to be handled
So, below is the before code of my hoc getInitialProps
static getInitialProps (ctx) {
const loggedUser = process.browser ? getUserFromLocalCookie() : getUserFromServerCookie(ctx.req)
const pageProps = Page.getInitialProps && Page.getInitialProps(ctx);
return {
...pageProps,
loggedUser,
currentUrl: ctx.pathname,
isAuthenticated: !!loggedUser
}
}
And the following is corrected code
static async getInitialProps (ctx) {
const loggedUser = process.browser ? getUserFromLocalCookie() : getUserFromServerCookie(ctx.req)
const pageProps = await (Page.getInitialProps && Page.getInitialProps(ctx));
return {
...pageProps,
loggedUser,
currentUrl: ctx.pathname,
isAuthenticated: !!loggedUser
}
}
I am basically trying to set the state based on the response I got from an API.
api.js
const baseURL = "http://localhost:8000";
export const getStatus = (list) => {
fetch(`${baseURL}/api/status`).then(res => {
return res.json();
}).then(status => {
list.setState({status: status});
});
};
And this is how I call it from a component
import React, {PropTypes} from 'react';
import {getStatus} from '../../../api';
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
status: [],
};
}
componentDidMount() {
getStatus(this);
}
I feel like it is not a good practice to pass this down and modify the state from the downstream api file. Is there a more "react" way to do this?
I also tried another way, which is to wait for the callback to send back the response and then modify the state based on the response, but the setState function never gets executed in componentDidMount. If someone can direct me, that would be great!
api.js
const baseURL = "http://localhost:8000";
export const getStatus = () => {
fetch(`${baseURL}/api/status`).then(res => {
return res.json();
}).then(status => {
return status;
});
};
import React, {PropTypes} from 'react';
import {getStatus} from '../../../api';
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
status: [],
};
}
componentDidMount() {
getStatus((status) => {
this.setState({status: status});
})
}
Better way is to use .then() in componentDidMount
api.js
export const getStatus = () => {
return fetch(`${baseURL}/api/status`).then(res => {
return res.json();
});
};
yourComponent.jsx
import React, {PropTypes} from 'react';
import {getStatus} from '../../../api';
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
status: [],
};
}
componentDidMount() {
getStatus()
.then(status => {
this.setState({status: status});
})
}
Making an API call and setting the state of a component based on what the call returns is normal practice in react. You don't need to pass a reference to this down to getStatus and for that matter you don't need to pass anything to getStatus. Instead, chain then off of what is returned from getStatus.
componentDidMount() {
getStatus()
.then(status => {
this.setState({status});
})
}
it is also unnecessary to call constructor or super in your component. Simply write:
class List extends React.Component {
state = {
status: []
}
}
If you are using ES6, try the async function syntax to increase readability.
api.js
export const getStatus = () => fetch(`${baseURL}/api/status`);
yourComponent.jsx
import React, {PropTypes} from 'react';
import {getStatus} from '../../../api';
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
status: [],
};
}
async componentDidMount() {
const res = await getStatus()
this.setState({status: res.json()});
}
Also, you might not need to initialize the state, and you could remove the constructor if so.
I have a working code:
fetch(serviceUrl)
.then(result => result.json())
.then(newStatus => this.setState({status: newStatus}))
.catch(error => {
console.log(error);
this.setState({status: 'error'});
})
I'm a little bit confused with vuex store component.
How should I obtain state of another module?
I tried a different ways to get data from store and always got Observer object. What is the correct way to knock knock to observer?
If I try to get anything from this object directly, like rootState.user.someVariable then I got undefined response.
Don't have a problem getting state from components.
Edit. Add code
User module
import * as Constants from './../../constants/constants'
import * as types from '../mutation-types'
import axios from 'axios'
const state = { user: [] }
const getters = {
getUser: state => state.user
}
const actions = {
getUserAction ({commit}) {
axios({method: 'GET', 'url': Constants.API_SERVER + 'site/user'})
.then(result => {
let data = result.data
commit(types.GET_USER, {data})
}, error => {
commit(types.GET_USER, {})
console.log(error.toString())
})
}
}
const mutations = {
[types.GET_USER] (state, {data}) {
state.user = data
}
}
export default { state, getters, actions, mutations }
Mutatinos
export const GET_LANGS = 'GET_LANGS'
export const GET_USER = 'GET_USER'
Store
import Vuex from 'vuex'
import Vue from 'vue'
import user from './modules/user'
import lang from './modules/lang'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
user,
lang
}
})
Main app
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store/index'
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})
Lang module, here is the place where I'm trying get store
import * as types from '../mutation-types'
import {axiosget} from '../../api/api'
const state = { langList: [] }
const getters = {
getLangs: state => state.langList
}
const actions = {
// this two action give me similar result
getLangsAction (context) {
axiosget('lang') // described below
},
getAnotherLangsAction (context) {
console.log(context.rootState.user) <----get Observer object
}
}
const mutations = {
[types.GET_LANGS] (state, {data}) {
state.langList = data
}
}
export default { state, getters, actions, mutations }
axiosget action, api module
import * as Constants from './../constants/constants'
import store from '../store/index'
import axios from 'axios'
export const axiosget = function (apiUrl, actionSuccess, actionError) {
console.debug(store.state.user) // <----get Observer object, previously described
// should append user token to axios url, located at store.state.user.access_token.token
axios({method: 'GET', 'url': Constants.API_URL + apiUrl
+ '?access_token=' + store.state.user.access_token.token})
.then(result => {
let data = result.data
// todo implement this
// }
}, error => {
if (actionError && actionError === 'function') {
// implement this
}
})
}
Component, that call dispatcher. If i get state via mapGetters in computed properties - there is no problems
<template>
<div>
{{user.access_token.token}}
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'ArticlesList',
computed: mapGetters({
user: 'getUser'
}),
created () {
this.$store.dispatch('getLangsAction')
this.$store.dispatch('getAnotherLangsAction')
}
}
</script>
What I'm trying to do in this code - get user access token in main site (after login) and all further manipulations with data will be produced via api host.
Let's say you want to fetch state an attribute userId from object userDetails in Vuex store module user.js.
userDetails:{
userId: 1,
username: "Anything"
}
You can access it in following way in action
authenticateUser(vuexContext, details) {
userId = vuexContext.rootState.user.userDetails.userId;
}
Note: After rootState and before file name user, add the path to the store module file if it is inside nested folders.
I'm trying to understand mobx implementation in React. I used create react app and update default configuration to use decorators. Then I created a simple store like this :
EDIT : after Ben Hare (thanks to him !) reply I updated my code like this :
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import MessageStore from "./store/messages";
ReactDOM.render(<App store={new MessageStore()} />,
document.getElementById('root'));
** App.js **
import React from "react";
import { observer } from "mobx-react";
#observer
export default class App extends React.Component {
constructor(props) {
super(props);
this.store = props.store;
}
render() {
return <ul>
{ this.store.allMessages.map((msg) => {
return <li key={msg}>{msg}</li>
})}
</ul>
}
}
messages.js
import {action, observable, computed} from "../../node_modules/mobx/lib/mobx";
export default class MessageStore {
#observable messages = ["My first message"];
constructor() {
setInterval(() => {
// Add some random messages every second
this.addMessage(Math.random());
}, 1000);
}
#action addMessage(msg) {
this.messages.push(msg);
}
#computed get allMessages() {
return this.messages;
}
}
The first message is displayed, but component never update when setInterval add message into the store. Can you help me ?
Works for me:
https://codesandbox.io/s/LgQXNBnNW
Did you see any errors in the browser log or terminal?
Check my approach please. Maybe it will help:
MobX store:
import { action, observable, runInAction } from 'mobx'
class DataStore {
#observable data = null
#observable error = false
#observable fetchInterval = null
#observable loading = false
//*Make request to API
#action.bound
fetchInitData() {
const response = fetch('https://poloniex.com/public?command=returnTicker')
return response
}
//*Parse data from API
#action.bound
jsonData(data) {
const res = data.json()
return res
}
//*Get objects key and push it to every object
#action.bound
mapObjects(obj) {
const res = Object.keys(obj).map(key => {
let newData = obj[key]
newData.key = key
return newData
})
return res
}
//*Main bound function that wrap all fetch flow function
#action.bound
async fetchData() {
try {
runInAction(() => {
this.error = false
this.loading = true
})
const response = await this.fetchInitData()
const json = await this.jsonData(response)
const map = await this.mapObjects(json)
const run = await runInAction(() => {
this.loading = false
this.data = map
})
} catch (err) {
console.log(err)
runInAction(() => {
this.loading = false
this.error = err
})
}
}
//*Call reset of MobX state
#action.bound
resetState() {
runInAction(() => {
this.data = null
this.fetchInterval = null
this.error = false
this.loading = true
})
}
//*Call main fetch function with repeat every 5 seconds
//*when the component is mounting
#action.bound
initInterval() {
if (!this.fetchInterval) {
this.fetchData()
this.fetchInterval = setInterval(() => this.fetchData(), 5000)
}
}
//*Call reset time interval & state
//*when the component is unmounting
#action.bound
resetInterval() {
if (this.fetchInterval) {
clearTimeout(this.fetchInterval)
this.resetState()
}
}
}
const store = new DataStore()
export default store