I have a connected component that maintains a display "state" along with a few other things that are needed for communication between a couple of components. I have two connected components that are children of this over-arching component. Depending on a flag that is within the "state" component one or the other child components will render. It might be better to just show the code:
EditorState Component:
import React from 'react';
import {connect} from 'react-redux';
import Library from '../library/index';
import Editor from '../editor/index';
import {
initialize,
editorState
} from './actions';
class EditorState extends React.Component {
componentWillMount() {
const {dispatch} = this.props;
dispatch(initialize());
}
render() {
const {state} = this.props;
switch(state) {
case editorState.Library:
return <Library />
case editorState.Editor:
return <Editor />
default:
return null;
}
}
}
export default connect(state => {
return state.EditorStateReducer;
})(EditorState);
EditorState Actions:
export const EDITOR_STATE_INITIALIZE = 'EDITOR_STATE_INITIALIZE';
export const editorState = {
Library: 'library',
Editor: 'editor'
}
export const initialize = ({
type: EDITOR_STATE_INITIALIZE,
state: editorState.Library
});
EditorState Reducer:
import {
EDITOR_STATE_INITIALIZE
} from './actions';
const init = () => ({
state: null
});
export default (state = init(), action) => {
switch(action.type) {
case EDITOR_STATE_INITIALIZE:
return {
...state,
state: action.state
}
default:
return {...state}
}
}
Library Component:
import React from 'react';
import {connect} from 'react-redux';
import {Page} from '../../../components/page/index';
import LibraryItem from '../../../components/library-item/library-item';
import {
initialize
} from './actions';
class Library extends React.Component {
componentWillMount() {
const {dispatch} = this.props;
dispatch(initialize());
}
render() {
const {templates} = this.props;
const editorTemplates = templates.map(template =>
<LibraryItem template={template} />
);
return (
<Page>
<div className="card-flex library-table">
{editorTemplates}
</div>
</Page>
)
}
}
export default connect(state => {
return state.LibraryReducer;
})(Library);
Library Actions:
import {
client,
serviceUrl
} from '../../../common/client';
export const LIBRARY_INITIALIZE = 'LIBRARY_INITIALIZE';
export const initialize = () => dispatch => {
client.get(`${serviceUrl}/templates`).then(resp => {
dispatch({
type: LIBRARY_INITIALIZE,
templates: resp.templates
});
});
}
Library Reducer:
import {
LIBRARY_INITIALIZE
} from './actions';
const init = () => ({
templates: []
});
export default (state = init(), action) => {
switch(action.type) {
case LIBRARY_INITIALIZE:
return {
...state,
templates: action.templates
}
default:
return {...state}
}
}
The problem that I am having is that the mapStateToProps in the Library Component is not being called upon the dispatch of LIBRARY_INITIALIZE. I have breakpoints in both mapStateToProps in the EditorState and Library, and a breakpoint in the LIBRARY_INITIALIZE switch in the Library reducer. Debugging page load goes like this:
EditorState mapStateToProps - state.EditorStateReducer.state is null
EditorState mapStateToProps - state.EditorStateReducer.state == editorState.Library
Library mapStateToProps - state.LibraryReducer.templates == []
Library Reducer Initialize - action.templates == [{template1}, {template2}, etc]
EditorState mapStateToProps - state.LibraryReducer.templates == [{template1}, {template2}, etc]
Then nothing. I would expect the Library mapStateToProps to fire as well after this so that the Library can re-render with the templates. However, this is not happening. Why is this not happening? I am ready to pull my hair out over this one...
You cannot be 100% sure that the updated state is rendered right after the dispatch call. mapStatetoProps is called when the component is about to re-render, which depends on whether React batches the updates or not. By default, React batches updates from event handlers.
You can refer https://github.com/reactjs/react-redux/issues/291
Related
Why I see always "loading..."?
I used redux-toolkit and createSlice and fetch data by axios.
I have not any problem by fetching data and my data is in State.
My problem is displaying fetched data.
My Component code is:
import React, {useEffect, useState} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {fetchTrackers} from 'dashboard/dashboardSlice';
export default function TrackerManagerDashboard() {
const [trackersList, setTrackersList] = useState(useSelector(state => state.trackersData));
const [activeTracker, setActiveTracker] = useState(useSelector(state => state.activeTracker));
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTrackers);
}, []);
if(!trackersList)
return (
<div>loading...</div>
)
return (
<div className="TrackerManagerDashboard">
...
</div>
)
}
and reducer Slice file is:
import { createSlice } from '#reduxjs/toolkit'
import * as env from "../../environments";
import axios from 'axios';
const initialState = {
trackersData: {},
activeTracker: {},
}
const dashboardSlice = createSlice({
name: 'dashboard',
initialState,
reducers: {
setInitialState(state, action) {
state.trackersData = action.payload.data;
state.activeTracker = state.trackersData[Object.keys(state.trackersData)[0]];
},
},
})
export async function fetchTrackers (dispatch, getState) {
try {
const { data } = await axios.get(env.APP_URL + '/fetch/trackers.json');
dispatch(setInitialState({type: 'setInitialState', data }));
} catch(error) {
console.log(error);
}
}
export const { setInitialState} = dashboardSlice.actions
export default dashboardSlice.reducer
Never use useState(useSelector.
That means "create a component-local state with the initial value that the return value of useSelector at the time of first render has". If the Redux state changes later, you will never get to see it, as your useState is already initialized and any change there will not be reflected in your trackersList variable.
Instead, just call useSelector:
const trackersList = useSelector(state => state.trackersData);
const setActiveTracker = useSelector(state => state.activeTracker);
I have created an input field and am dispatching an action in the onChange method of the input field. i have a state attribute text:'' which should get updated once user starts typing in the input field but same is not happening , instead text attribute in the state disappears once action is fired as checked in Redux Dev tool.
Also have below 2 queries - read the docs , but still not clear
Why do we have to pass initialState in createStore as I have already passed in state to reducers , though have passed empty initialState to createStore .
Do I need to use mapStateToProps in my case as I am not making use of any state in my component
Action file - searchActions.js
import { SEARCH_MOVIE } from "./types";
export const searchMovie = text => ({
type: SEARCH_MOVIE,
payload: text
});
Reducer file - searchReducer.js
import { SEARCH_MOVIE } from "../actions/types";
const initialState = {
text: ""
};
const searchReducer = (state = initialState, action) => {
console.log(action);
switch (action.type) {
case SEARCH_MOVIE:
return {
...state,
text: action.payload
};
default:
return state;
}
};
export default searchReducer;
Root reducer file - index.js
import { combineReducers } from "redux";
import searchReducer from "./searchReducer";
const rootReducer = combineReducers({
searchReducer
});
export default rootReducer;
Store file - store.js
import { createStore } from "redux";
import rootReducer from "./reducers";
const initialState = {};
const store = createStore(
rootReducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
Form containing the input field - SearchForm.js
import React, { Component } from "react";
import { searchMovie } from "../../actions/searchActions";
import { connect } from "react-redux";
export class SearchForm extends Component {
onChange = e => {
this.props.searchMovie(e.target.value);
};
render() {
return (
<div className="jumbotron jumbotron-fluid mt-5 text-center">
<form className="form-group">
<input
type="text"
className="form-control"
placeholder="Search Movies"
onChange={e => this.onChange(e)}
></input>
<button type="submit" className="btn btn-primary mt-3 btn-bg">
Search
</button>
</form>
</div>
);
}
}
const mapStateToProps = state => {
return {
text: state.text
};
};
const mapDispatchToProps = dispatch => ({
searchMovie: () => dispatch(searchMovie())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearchForm);
Entry file - App.js
import React, { Component } from "react";
import { Provider } from "react-redux";
import SearchForm from "./SearchForm";
import store from "./store";
import "./App.css";
class App extends Component {
render() {
return (
<Provider store={store}>
<SearchForm />
</Provider>
);
}
}
export default App;
You call this function with a parameter
this.props.searchMovie(e.target.value);
However in here you do not provide any parameter
const mapDispatchToProps = dispatch => ({
searchMovie: () => dispatch(searchMovie())
});
should be
const mapDispatchToProps = dispatch => ({
searchMovie: movie => dispatch(searchMovie(movie))
});
For your questions
1. Why do we have to pass initialState in createStore as I have already passed in state to reducers , though have passed empty initialState to createStore .
You haven't passed initialState to reducer, in fact you can't. This is different const searchReducer = (state = initialState, action) => {. It's a javascript syntax which acts as a default value for a parameter if you don't provide one.
2. Do I need to use mapStateToProps in my case as I am not making use of any state in my component
Yes you don't have to. You can set is as undefined
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()
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.
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