useEffect not grabbing current state - react-hooks

So I have a state that is set to some fetched data, which I then store a copy of into a variable within a useEffect. I then use the data within that variable to map out elements.
import React, { useContext, useEffect } from 'react'
import { newContext } from './Context'
import { PrivateChatArea } from './PrivateChatArea'
import { PrivateChatContent } from './PrivateChatContent'
function PrivateMessages() {
const { pmData } = useContext(newContext)
let mutable = { ...pmData }
useEffect(() => {
console.log(pmData)
mutable = { ...pmData }
console.log(mutable)
}, [pmData])
return (
<>
<PrivateChatArea>
{Object.keys(mutable).length !== 0 && mutable.messages.map((messages, key) => {
return (
<PrivateChatContent key={key} id={messages.messageID}>{messages.message} </PrivateChatContent>
)
})}
</PrivateChatArea>
</>
)
}
I was hoping useEffect would grab and store the current state(pmData) on state change, however based on the logs useEffect seems to be grabbing the previous state instead of the current state. Causing the mapped elements to be inaccurate.
Refreshing immediately fixes this, but I would rather not have to refresh just to see the updated state changes.

Related

State changes are not being reflected in ReactJS

So here I have some elements mapped out based on state. I need the elements to reflect the current state after I set state. After hours of researching, I figured updating the variable with a spread operator would force my component to re-render when I set state with said variable. However, the state changes are not being reflected without refreshing.
const updateCurrentChat = () => {
let filtered = {}
findChat.forEach((chat) => {
chat.psuedoID.includes(userInfo._id) &&
chat.psuedoID.includes(currentTarget._id)
? (filtered = { ...chat }) // updating variable
: console.log(chat)
})
setPMData(filtered) // setting state
}
function PrivateMessages() {
const { pmData } = useContext(newContext)
return (
<>
<PrivateChatArea>
{pmData
? pmData.messages.map((messages) => {
return (
<PrivateChatContent id={messages.messageID}>
{messages.message}
</PrivateChatContent>
)
})
: null}
</PrivateChatArea>
</>
)
}
Hi #Romeo Richardson,
You can use useEffect hook to ensure that the component re-renders when the pmData state changes.
function PrivateMessages() {
const { pmData } = useContext(newContext)
const [, setRerender] = useState(0)
useEffect(() => {
setRerender((rerender) => rerender + 1)
}, [pmData])
return (
<>
<PrivateChatArea>
{pmData
? pmData.messages.map((messages) => {
return (
<PrivateChatContent id={messages.messageID}>
{messages.message}
</PrivateChatContent>
)
})
: null}
</PrivateChatArea>
</>
)
}
Alternatively, if you don't want to use useEffect. Then, you can use React.memo. You only need to export your component like this export default React.memo(PrivateMessages);.

"Objects are not valid as a React child" Redux error when conditionally connecting a component?

I'm attempting to load a component with Redux only when the environmental variable USE_MOCK_PROPS is set to false. If it's true then I'll render the component without Redux but with some mock props instead.
import React from "react";
import Page from "./page";
import { connect } from "react-redux";
const { USE_MOCK_PROPS } = process.env;
const mockProps = {
foo: 'bar'
}
export default function() {
if (USE_MOCK_PROPS) {
return <Page {...mockProps} />;
}
const mapStateToProps = state => {
return {
state
};
};
return connect(mapStateToProps)(Page);
}
The non-Redux part is working fine however when trying to use Redux I get this error:
Objects are not valid as a React child (found: object with keys
{$$typeof, type, compare, WrappedComponent, displayName}). If you
meant to render a collection of children, use an array instead.
import React from "react";
import Page from "./page";
import { connect } from "react-redux";
const { USE_MOCK_PROPS } = process.env;
const mockProps = {
foo: 'bar'
}
function mockedComponent() {
return <Page {...mockProps} />;
}
function connectedComponent() {
const mapStateToProps = state => {
return {
state
};
};
return connect(mapStateToProps)(Page)
}
export default USE_MOCK_PROPS ? mockedComponent : connectedComponent()

Why is my React view not updating when the redux store changes?

I'm new to React and Redux. I've been trying to create a counter to learn how to work with Redux, but I cannot get the view to update, or mapStateToProps to be called. I've gone over the Redux troubleshooting page, and looked at a number of similar threads posted here. Could somebody please help me identify what is wrong with this code?
import React from 'react'
import { render } from 'react-dom'
import { Provider, connect } from 'react-redux'
import { createStore } from 'redux'
import './index.scss';
let Counter = props => {
console.log('>>> counter', props)
return (
<div>
<h1>counter (Redux Version)</h1>
<p>count: {props.count}</p>
</div>
);
}
let increment = () => {
return {
type: 'INC'
}
}
const counterReducer = (state = {count: 0}, action) => {
switch (action.type) {
case 'INC':
console.log('>>> inc');
return { count: state.count + 1 };
default:
return state;
}
}
const mapStateToProps = state => {
console.log('>>> map state to props', state);
return { count: state.counterReducer.count };
};
const store = createStore(counterReducer);
connect(mapStateToProps, null)(Counter);
render(
<Provider store={store}>
<Counter count={0}/>
</Provider>,
document.getElementById('root')
);
window.store = store;
window.increment = increment;
Using the Chrome console, I am able to see the sate of the store, and then dispatch the increment event. When I do this, I see that the state in the store has updated correctly but the value shown in the paragraph tag still says 0.
store.getState()
{count: 0}
store.dispatch(increment());
VM1382:1 >>> inc
{type: "INC"}
store.getState()
{count: 1}
Counter isn't updating on dispatch because Counter in render isn't actually a connected component.
According to the docs on connect (https://react-redux.js.org/api/connect):
It does not modify the component class passed to it; instead, it returns a new, connected component class that wraps the component you passed in.
This means that simply calling connect(mapStateToProps, null)(Counter); will not mutate Counter, but it'll instead return a connected version of it instead. This returned component is what you want.
A quick fix to this would be to save connect's return value:
const ConnectedCounter = connect(mapStateToProps)(Counter);
and use ConnectedCounter instead of Counter in render:
<Provider store={store}>
<ConnectedCounter count={0}/>
</Provider>
When connecting components to the store, I suggest implementing the component in its own jsx file and export the connected component as default.
In Counter.jsx, we can have...
import { connect } from 'react-redux';
const Counter = props => {
console.log('>>> counter', props)
return (
<div>
<h1>counter (Redux Version)</h1>
<p>count: {props.count}</p>
</div>
);
}
const mapStateToProps = state => {
console.log('>>> map state to props', state);
return { count: state.counterReducer.count };
};
export default connect(mapStateToProps)(Counter);
...so whenever Counter is imported somewhere else in the project (import Counter from '<path>/Counter'), we're sure that Counter is already a connected component.

componentDidUpdate does not fire

So I've been struggling to figure out the react-redux ecosystem for a while now. I'm almost there but there is still something that keep giving is me issues, and that's the componentDidUpdate method. When I dispatch an async action, the store is reducer is called correctly and the component's state does update.
But for some reason, the componentDidUpdate method does not fire, there is no re-render, and I cannot access the updated props. I can see it change in devtools, if I console.log(this.props.blogStore). At first it shows as an empty object but when on click it opens and shows the updated state.
I've tried as many life cycle methods as I can but nothing seems to work, including componentWillReceiveProps.
Any idea what I'm doing wrong?
Here is the code:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import Datastore from 'Datastore';
const store = Datastore()
store.subscribe(() => console.log("state changed", store.getState()))
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);
Datastore.js
import { combineReducers, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import Mainmenu from 'reducers/Mainmenu';
import Blogstore from 'reducers/Blogstore';
const reducer = combineReducers({
Mainmenu,
Blogstore,
})
export default function Datastore() {
const store = createStore(
reducer,
applyMiddleware(thunk)
)
return store
}
reducer
import Article from 'lib/Article';
import { ARTICLE_LOAD, ARTICLE_UPDATE, SAVE_ARTICLE_LIST } from 'actionTypes';
const initialBlogState = {
}
const Blogstore = (state=initialBlogState, action) => {
switch(action.type) {
case SAVE_ARTICLE_LIST:
state.init = true
state.articles = action.payload
return state
case ARTICLE_LOAD:
return state
case ARTICLE_UPDATE:
return state
}
return state
}
export default Blogstore;
blog-actions.js
import { ARTICLE_LOAD, ARTICLE_UPDATE, SAVE_ARTICLE_LIST } from 'actionTypes';
import APIFetch from '../lib/Fetch';
export function getArticlePids() {
return dispatch => {
APIFetch().get("/blog/list").then(response => {
dispatch({
type: SAVE_ARTICLE_LIST,
payload: response.data
})
})
}
}
component
import React from 'react';
import { connect } from 'react-redux';
import * as blogActions from '../actions/blog-actions';
#connect(state => ({
blogStore: state.Blogstore
}))
export default class Blog extends React.Component {
constructor() {
super()
}
componentDidMount() {
this.props.dispatch(blogActions.getArticlePids())
}
componentDidUpdate(prevProps) {
console.log("update", prevProps)
}
render() {
console.log("render", this.props.blogStore)
return (
<div><h1>Blog</h1></div>
)
}
}
That is pretty much it. I won't bother pasting the App and Router that are between index.js and the component because there is nothing of interest there. Just a basic react router and components that have nothing to do with this.
You need to return a new object from your reducer, like this:
import Article from 'lib/Article';
import { ARTICLE_LOAD, ARTICLE_UPDATE, SAVE_ARTICLE_LIST } from 'actionTypes';
const initialBlogState = {
}
const Blogstore = (state=initialBlogState, action) => {
switch(action.type) {
case SAVE_ARTICLE_LIST:
return Object.assign({}, state, {
init: true,
articles: action.payload,
})
case ARTICLE_LOAD:
return state
case ARTICLE_UPDATE:
return state
}
return state
}
export default Blogstore;
Otherwise, if you try to update your state directly (as you are doing currently) it will only mutate the internal reference of the state and react components won't be able to detect the change and wont re-render. Read more here.

react-redux initial ajax data and generate childrens

I am new to react-redux.I have to says I read a lot of example project, many use webpack and couple a lot of package together without detailed introduction. I also read official example several times, but I still can not understand it well, specially in how to get initial data, and show it in the dom and communicate with ajax(not like jquery.ajax, use ajax in redux seems very complex, everyone's code has different approach and different style make it much hard to understand)
I decide to build a file manager webui to learn react-redux.
To begin, I just want it work, so no ajax:
containers/App.js:
import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {getFileList} from '../actions/NodeActions'
import Footer from '../components/Footer';
import TreeNode from '../containers/TreeNode';
import Home from '../containers/Home';
export default class App extends Component {
componentDidMount() {
let nodes = getFileList();
this.setState({
nodes: nodes
});
}
render() {
const { actions } = this.props;
const { nodes } = this.state;
return (
<div className="main-app-container">
<Home />
<div className="main-app-nav">Simple Redux Boilerplate</div>
{nodes.map(node =>
<TreeNode key={node.name} node={node} {...actions} />
)}
<Footer />
</div>
);
}
}
function mapStateToProps(state) {
return {
test: state.test
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(getFileList, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
actions/NodeActions.js:
import { OPEN_NODE, CLOSE_NODE } from '../constants/ActionTypes';
export function openNode() {
return {
type: OPEN_NODE
};
}
export function closeNode() {
return {
type: CLOSE_NODE
};
}
class NodeModel {
constructor(name, path, type, right) {
this.name = name;
this.path = path;
this.type = type;
this.right = right;
}
}
const testNodes = [
new NodeModel('t1','t1', 'd', '777'),
new NodeModel('t2','t2', 'd', '447'),
new NodeModel('t3','t3', 'd', '667'),
]
export function getFileList() {
return {
nodes: testNodes
}
}
export function ansyncGetFileList() {
return dispatch => {
setTimeout(() => {
dispatch(getFileList());
}, 1000);
};
}
reducers/index.js
import { combineReducers } from 'redux';
import opener from './TreeNodeReducer'
const rootReducer = combineReducers({
opener
});
export default rootReducer;
reducers/TreeNodeReducer.js
import { OPEN_NODE, CLOSE_NODE } from '../constants/ActionTypes';
const initialState = [
{
open: false
}
]
export default function opener(state = initialState, action) {
switch (action.type) {
case OPEN_NODE:
return true;
case CLOSE_NODE:
return false;
default:
return state;
}
}
reducers/index.js
import { combineReducers } from 'redux';
import opener from './TreeNodeReducer'
const rootReducer = combineReducers({
opener
});
export default rootReducer;
store/store.js(a copy from a redux demo):
import { createStore, applyMiddleware, compose } from 'redux';
import rootReducer from '../reducers';
import createLogger from 'redux-logger';
import thunk from 'redux-thunk';
import DevTools from '../containers/DevTools';
const logger = createLogger();
const finalCreateStore = compose(
// Middleware you want to use in development:
applyMiddleware(logger, thunk),
// Required! Enable Redux DevTools with the monitors you chose
DevTools.instrument()
)(createStore);
module.exports = function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
// Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers'))
);
}
return store;
};
chrome console says:Uncaught TypeError: Cannot read property 'nodes' of null at App render() {
I don't know the es6 well, due to react-redux strange syntax make me read the es6 doc, but I am not sure my code is right.
Tring:
I think maybe can not create testNodes with new instance in the list, so I change testNodes to plain json:
const testNodes = [
{name:'t1',type:'t1'},
{name:'t2',type:'t2'},
{name:'t3',type:'t3'},
]
Still same error
maybe action can not get the global testNodes? I move testNodes into getFileList, not work too.
I have no idea.
After solve this, I would try to replace getFileList content to a ajax call.
PS:My react-route also have strange problem, chrome show blank page and no error when I wrap App with route, just feel react-redux is so hard for newbee...this is just some complain...
Simply
you don't need to bindActionCreators yourself
you need to use this.props.getFileList
you don't need to manage it with component's state
for eg.
import {ansyncGetFileList} from '../actions/NodeActions'
componentWillMount() {
// this will update the nodes on state
this.props.getFileList();
}
render() {
// will be re-rendered once store updated
const {nodes} = this.props;
// use nodes
}
function mapStateToProps(state) {
return {
nodes: state.nodes
};
}
export default connect(
mapStateToProps,
{ getFileList: ansyncGetFileList }
)(App);
Great Example
Update based on the question update and comment
since your state tree doesn't have a map for nodes you'll need to have it in the state's root or opener sub tree.
for async operation you'll have to modify your thunk action creator
for eg.
export function ansyncGetFileList() {
return dispatch => {
setTimeout(() => {
dispatch({ type: 'NODES_SUCCESS', nodes: getFileList()}); // might need to export the type as constant
}, 1000);
};
}
handle the NODES_SUCCESS action type in reducer
const initialState = {
nodes: []
};
export default function nodes(state = initialState, action) {
switch (action.type) {
// ...
case 'NODES_SUCCESS':
let nodes = state.nodes.slice();
return nodes.concat(action.nodes);
// ...
}
}
use nodes reducer to manage nodes sub tree
for eg.
import { combineReducers } from 'redux';
import opener from './TreeNodeReducer'
import nodes from './nodes'
const rootReducer = combineReducers({
opener, nodes
});
export default rootReducer;
use mapStateToProps as above to get the nodes
regarding mapDispatchToProps
The only use case for bindActionCreators is when you want to pass some action creators down to a component that isn’t aware of Redux, and you don’t want to pass dispatch or the Redux store to it.
Since you already have the access to dispatch you can call it directly. Passing a map is a shorthand version of it. video

Resources