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
Related
I am trying to create a search functionality.
So the values from the search input is actually getting passed in my actions and I can see the values from redux logger. However redux saga seems not able to intercept the payload value from the action creator. When I console log it it prints undefined.
Actions
//ACTIONS
import SearchActionTypes from "./search.types";
export const SearchActionStart = (value) => ({
type: SearchActionTypes.SEARCH_START,
value
});
export const SearchActionSuccess = (items) => ({
type: SearchActionTypes.SEARCH_SUCCESS,
payload: items,
});
export const SearchActionFailure = (e) => ({
type: SearchActionTypes.SEARCH_FAILURE,
payload: e,
});
Search Component
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectFieldData } from "../../redux/search/search.selector";
import { SearchActionStart } from "../../redux/search/search.actions";
const SearchComponent = (props) => {
const { searchResults, value } = props;
useEffect(() => {}, []);
const onSearchChange = (event) => {
const { value } = event.target;
searchResults(value);
};
return (
<div>
<input
type="text"
value={value}
onChange={onSearchChange}
/>
</div>
);
};
const mapDispatchToProps = (dispatch) => ({
searchResults: (value) =>
dispatch(SearchActionStart(value)),
});
const mapStateToProps = createStructuredSelector({
searchItem: selectFieldData,
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearchComponent);
searchSaga
import {
put,
call,
takeLatest,
all,
} from "redux-saga/effects";
import { SearchImage } from "../../api/search-image";
import Axios from "axios";
import {
SearchActionStart,
SearchActionSuccess,
SearchActionFailure,
} from "./search.actions";
import SearchActionTypes from "./search.types";
function* fetchFieldAsync(value) {
try {
// const images = yield call(SearchImage, value);
console.log(value);
// yield put(SearchActionSuccess(value));
} catch (e) {
yield put(SearchActionFailure(e));
console.log(e);
}
}
export function* fetchFieldStart() {
yield takeLatest(
SearchActionTypes.SEARCH_START,
fetchFieldAsync
);
}
export function* searchFieldSaga() {
yield all([call(fetchFieldAsync)]);
}
rootSaga
import { call, all } from "redux-saga/effects";
import { searchFieldSaga } from "./search/search.saga";
export default function* rootSaga() {
yield all([call(searchFieldSaga)]);
}
Please have a look into this code sandbox(https://codesandbox.io/s/basic-redux-saga-49xyd?file=/index.js) ... Your code is working fine. In saga function you will get the object that has been sent from the action as the param. You can destructure it into {value} to get the search term alone as param instead of action object.
A very silly mistake.
In my searchSaga instead of exporting the watcher function fetchFieldStart function. I mistakenly exported the intermediary functions instead, which is the fetchFieldAsync function whose job is to fetch an API.
So in
searchSaga.js
instead of:
export function* searchFieldSaga() {
yield all([call(fetchFieldAsync)]);
}
It should be:
export function* searchFieldSaga() {
yield all([call(fetchFieldStart)]);
}
For anyone who might encounter undefined error in your sagas, it might be worth reviewing if your exporting correct functions.
I hope this could also help anyone who have encountered similar problem
Thanks evryone.
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()
I learn React-Redux and need help understanding why this Component only works on start but not when I press the button.
When debug start the breakpoints in the picture break execution but when I press the button I get this error showed in the picture.
When breakpoints hit I hoower over the {toasts.map(toast => { and the Array size is zero. But when I press button the breakpoints does not even hit
Any ide?
UPDATE
I have this configureStore.js
import { combineReducers } from "redux";
import { createStore, applyMiddleware, compose } from "redux";
import { forbiddenWordsMiddleware } from "../middleware";
import ToastsReducer from '../reducers/ToastsReducer';
import RootReducer from '../reducers/RootReducer';
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const reducers = {
toastsReducer: ToastsReducer,
rootReducer: RootReducer
};
const reduce = combineReducers({
...reducers,
});
const store = createStore(
reduce,
storeEnhancers(applyMiddleware(forbiddenWordsMiddleware))
);
export default store;
RootReducer.js
import { ADD_ARTICLE } from "../constants/action-types";
import { FOUND_BAD_WORD } from "../constants/action-types";
const initialState = {
articles: []
};
export default function reducer(state = initialState, action) {
if (action.type === ADD_ARTICLE) {
return Object.assign({}, state, {
articles: state.articles.concat(action.payload)
});
}
if (action.type === FOUND_BAD_WORD) {
//return Object.assign({}, state, {
// articles: state.articles.concat(action.payload)
// });
}
return state;
}
ToastsReducer.js
import { ADD_TOAST, REMOVE_TOAST } from "../constants/action-types";
const initialState = {
toastList: []
};
export default function toasts(state = initialState, action) {
const { payload, type } = action;
switch (type) {
case ADD_TOAST:
return [payload, state.toastList];
case REMOVE_TOAST:
return state.toastList.filter(toast => toast.id !== payload);
default:
return state;
}
}
UPDATE
Picture showing RootReducer.jsx and Toasts.jsx when I press button two times,
Toast.js
import PropTypes from "prop-types";
import React, { Component } from "react";
class Toast extends Component {
render() {
return (
<li className="toast" style={{ backgroundColor: this.props.color }}>
<p className="toast__content">
{this.props.text}
</p>
<button className="toast__dismiss" onClick={this.props.onDismissClick}>
x
</button>
</li>
);
}
shouldComponentUpdate() {
return false;
}
}
Toast.propTypes = {
color: PropTypes.string.isRequired,
onDismissClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired
};
export default Toast;
Please share your reducer code. Most likely, you have not set an initial state for toastList in the reducer or there is an error with toastsReducer.toastList.
Try the following:
Change line 34 to toasts: state.toastsReducer
Comment the lines from 10 to 19 and insert the following to make sure toasts is an array.
console.log(toasts);
console.log(toasts.toastList);
return null;
If both are undefined, then the value returned by the reducer is not right.
In ToastsReducer.js:
Change the following:
case ADD_TOAST:
return [ ...state.toastList, payload]; //<--- Here
When you do return[payload,state.toastList], it appends another array to the toastList.
Run the following to see:
toastList = ['abc'];
// Right way to add an item to an array.
toastList = [...toastList, 'def'];
console.log(toastList);
console.log('-----');
// Adds an array to the array. Incorrect way.
toastList = [toastList, 'ghi'];
console.log(toastList);
---UPDATE---
Change your ADD_TOAST case to:
return { toastList: [...state.toastList, payload] };
and you should be good to go.
Just do check your toasts array contains data,
{toasts && toasts.length > 0 ? toasts.map(toast => {...}) : null}
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
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.