I'm beginner in react and redux, I have action which posts JSON on API and then receives list, this action called from button click, this all process works good but after populating data ui is not updating
Action:
import * as types from './actionTypes'
import { postMessage } from '../api/messaging'
function postToAPI(msg, dispatch) {
dispatch({ type: types.MESSAGE_POSTING });
postMessage(msg, (messages) => {
dispatch({
type: types.MESSAGE_POST_DONE,
messages: messages
});
});
}
export function postMessageAction(msg) {
return (dispatch) => {
postToAPI(msg, dispatch);
}
}
Reducer:
import * as types from '../actions/actionTypes'
const initialState = {
messages: []
}
export default function messages(state = initialState, action) {
switch(action.type) {
case types.MESSAGE_POST_DONE:
return {
...state,
messages: action.messages
}
this.forceUpdate();
default:
return state;
}
}
Main container:
export default class App extends Component {
render() {
return (
<Provider store={store}>
<CounterApp />
</Provider>
);
}
}
CounterApp:
class CounterApp extends Component {
constructor(props) {
super(props);
}
render() {
const { state, actions } = this.props;
return (
<Messaging />
);
}
}
export default connect(state => ({
messages: state.default.messages.messages
}))(CounterApp);
Messaging:
class Messaging extends Component {
render() {
return (
<View>
<MessageList messages={this.props.messages} />
<Message />
</View>
)
}
}
export default connect(state => ({
messages: state.default.messages.messages
}))(Messaging);
Message list:
export default class MessageList extends Component {
constructor(props) {
super(props);
}
render() {
return (
<ScrollView>
{
this.props.messages.map((item, index) => {
return (
<Text>
{ item.body }
</Text>
)
})
}
</ScrollView>
)
}
}
My MessageList component does not updates when messages changed. I read difference between props and state but i dont know how to pass data to state.
Update:
My state in messaging connect looks like this why i used default
Any ideas?
Your code looks strange. Firstly you need to connect to redux only in one component "Messaging"
import { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
const mapStateToProps = state => ({
messages: state.messages.messages
});
#connect(mapStateToProps);
class Messaging extends Component {
static propTypes = {
messages: PropTypes.object
}
render() {
const { messages } = this.props;
return (
<View>
<MessageList messages={messages} />
<Message />
</View>
)
}
}
Then use MessageList like dumb component to receive and render data.
export default class MessageList extends Component {
constructor(props) {
super(props);
}
renderMessages(item, index) {
return <Text>{item.body}</Text>;
}
render() {
const { messages } = this.props;
return (
<ScrollView>
{messages.map((item, index) => this.renderMessages(item, index))}
</ScrollView>
);
}
}
At a guess I'd say your connect statement wants to be
messages: state.messages
rather than
messages: state.default.messages.messages.
Also from what I can see I don't think you need the connect statement in CounterApp, it's not doing anything.
I'm not sure if the returned messages should replace or be merged with the existing messages but your reducer should be either
case types.MESSAGE_POST_DONE:
return {
messages: action.messages
}
if it's replacing the existing list or
case types.MESSAGE_POST_DONE:
return {
messages: [...state.messages, ...action.messages]
}
if you want to merge them.
A few things I noticed are:
From what I can see there's no default object in the state (you wrote messages: state.default.messages.messages).
You shouldn't use forceUpdate() in your reducer.
While it won't break anything, the CounterApp component is using connect without using any of the props.
Try this instead:
Reducer:
import * as types from '../actions/actionTypes'
const initialState = {
messages: []
}
export default function messages(state = initialState, action) {
switch(action.type) {
case types.MESSAGE_POST_DONE:
return {
...state,
messages: action.messages
}
default:
return state;
}
}
CounterApp:
class CounterApp extends Component {
render() {
return (
<Messaging />
);
}
}
Messaging:
class Messaging extends Component {
render() {
return (
<View>
<MessageList messages={this.props.messages} />
<Message />
</View>
)
}
}
export default connect(state => ({
messages: state.messages.messages
}))(Messaging);
Related
I have the following code:
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
const closeModal = id => ({
payload: {
id
},
type: 'CLOSE_MODAL'
});
const cfgAction = (a, b, c) => ({
payload: {
a,
b,
c
},
type: 'CFG_ACTION'
});
class classA extends Component {
constructor(props) {
super(props);
this.doAction = this.doAction.bind(this);
}
doAction() {
this.refs['aaa'].doOnRefItem();
}
render() {
const { modals, closeModal } = this.props;
// ...
const buttons = <div className="buttons">
<a onClick={() => closeModal("...")}>THIS WORKS</a>
<a onClick={this.doAction}>THIS WORKS</a>
</div>;
return <div>
<classB ref={'aaa'} />
<classB ref={'bbb'} />
{buttons}
</div>;
}
}
class classB extends Component {
constructor(props) {
super(props);
}
doOnRefItem() {
const { cfgAction } = this.props;
cfgAction("xxx", 5, true); //! *ERROR cfgAction is not a function*
}
render() {
// ...
}
}
const mapStateToProps = ({ modals }, ownProps) => (
{ modals }
);
const mapDispatchToProps = (dispatch, ownProps) =>
bindActionCreators(
{
closeModal,
cfgAction
},
dispatch
);
connect(mapStateToProps, mapDispatchToProps)(classB);
export default connect(mapStateToProps, mapDispatchToProps)(classA);
NB: all is in the same file (not the actions).
I don't know why I can't access to the props to the function.
A solution could be do another file with the classB but in that case doesn't work this.refs['aaa'].doOnRefItem(); because it can't find the function (but access to the element).
I wish understand in both cases why doesn't work and how fix it.
Thanks
UPDATE 1
I did the following change getting the ref error:
class ClassA extends Component {
constructor(props) {
super(props);
this.doAction = this.doAction.bind(this);
this.r1 = React.createRef();
this.r2 = React.createRef();
}
doAction() {
// ERROR TypeError: r1.current.doOnRefItem is not a function
this.r1.current.doOnRefItem();
this.rr.current.doOnRefItem();
}
render() {
const { modals, closeModal } = this.props;
// ...
const buttons = <div className="buttons">
<a onClick={() => closeModal("...")}>THIS WORKS</a>
<a onClick={this.doAction}>THIS WORKS</a>
</div>;
return <div>
<ConnectedClassB ref={this.r1} />
<ConnectedClassB ref={this.r2} />
{buttons}
</div>;
}
}
class ClassB extends Component {
constructor(props) {
super(props);
}
doOnRefItem() {
// ...
}
render() {
// ...
}
}
const mapStateToProps = ({ modals }, ownProps) => (
{ modals }
);
const mapDispatchToProps = (dispatch, ownProps) =>
bindActionCreators(
{
closeModal,
cfgAction
},
dispatch
);
const ConnectedClassB = connect(mapStateToProps, mapDispatchToProps)(ClassB);
export default connect(mapStateToProps, mapDispatchToProps)(ClassA);
ERROR TypeError: r1.current.doOnRefItem is not a function. Logging 'r1.current' I can see the class object...
UPDATE 2
Fixed with the following change:
const ConnectedClassB = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })(ClassB);
I see at least a couple issues immediately.
First, note that your React component names start with lower-case letters. React components should always start with an upper-case letter, so that the JSX transform works correctly. Change classA and classB to ClassA and ClassB everywhere you're using them.
Second, remember that connect() returns a new component type. You are calling connect(mapState, mapDispatch)(ClassB), but not using the returned component type anywhere - it's being thrown away. Instead, you'd need:
const ConnectedClassB = connect(mapState, mapDispatch)(ClassB);
and then your ClassA component needs to actually render that connected component type:
<ConnectedClassB />
In addition, React string refs are basically deprecated. Use object refs via React.createRef() instead.
Finally, note that you should really be using the "object shorthand" form of mapDispatch instead of the function form.
I want to push state to the browser and append to the pathname when a subreddit has changed.
In the example below the user chooses an option from ['reactjs', 'frontend']. So when the user chooses reactjs, I want to changethe browser url to: <url>/reddit/reactjs or <url>/reddit/frontend based on the selection.
So when the user goes back and forward, I want to show data that was already fetched.
How can I make it work with react-redux for the example below? Normally, I was using history.pushState(...).
Note: I am using connected-react-router
index.js:
import 'babel-polyfill'
import React from 'react'
import { render } from 'react-dom'
import Root from './containers/Root'
render(<Root />, document.getElementById('root'))
action.js:
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function selectSubreddit(subreddit) {
return {
type: SELECT_SUBREDDIT,
subreddit
}
}
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
function shouldFetchPosts(state, subreddit) {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}
export function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
}
reducers.js:
import { combineReducers } from 'redux'
import {
SELECT_SUBREDDIT,
INVALIDATE_SUBREDDIT,
REQUEST_POSTS,
RECEIVE_POSTS
} from './actions'
function selectedSubreddit(state = 'reactjs', action) {
switch (action.type) {
case SELECT_SUBREDDIT:
return action.subreddit
default:
return state
}
}
function posts(
state = {
isFetching: false,
didInvalidate: false,
items: []
},
action
) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
return Object.assign({}, state, {
didInvalidate: true
})
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
})
default:
return state
}
}
function postsBySubreddit(state = {}, action) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
default:
return state
}
}
const rootReducer = combineReducers({
postsBySubreddit,
selectedSubreddit
})
export default rootReducer
configureStore.js
import { createStore, compose, applyMiddleware } from 'redux'
import { createBrowserHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
import thunkMiddleware from 'redux-thunk'
import logger from 'redux-logger'
import rootReducer from '../reducers'
// const loggerMiddleware = createLogger()
export const history = createBrowserHistory()
export default function configureStore(preloadedState?: any) {
const store = createStore(
rootReducer(history), // root reducer with router state
preloadedState,
compose(
applyMiddleware(
thunkMiddleware,
logger,
routerMiddleware(history), // for dispatching history actions
// ... other middlewares ...
),
),
)
return store
}
Root.js
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import configureStore from '../configureStore'
import AsyncApp from './AsyncApp'
const store = configureStore()
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<AsyncApp />
</Provider>
)
}
}
AsnycApp.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
selectSubreddit,
fetchPostsIfNeeded,
invalidateSubreddit
} from '../actions'
import Picker from '../components/Picker'
import Posts from '../components/Posts'
class AsyncApp extends Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.handleRefreshClick = this.handleRefreshClick.bind(this)
}
componentDidMount() {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
componentDidUpdate(prevProps) {
if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
}
handleChange(nextSubreddit) {
this.props.dispatch(selectSubreddit(nextSubreddit))
this.props.dispatch(fetchPostsIfNeeded(nextSubreddit))
}
handleRefreshClick(e) {
e.preventDefault()
const { dispatch, selectedSubreddit } = this.props
dispatch(invalidateSubreddit(selectedSubreddit))
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
render() {
const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
return (
<div>
<Picker
value={selectedSubreddit}
onChange={this.handleChange}
options={['reactjs', 'frontend']}
/>
<p>
{lastUpdated && (
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '}
</span>
)}
{!isFetching && (
<button onClick={this.handleRefreshClick}>Refresh</button>
)}
</p>
{isFetching && posts.length === 0 && <h2>Loading...</h2>}
{!isFetching && posts.length === 0 && <h2>Empty.</h2>}
{posts.length > 0 && (
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
)}
</div>
)
}
}
AsyncApp.propTypes = {
selectedSubreddit: PropTypes.string.isRequired,
posts: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number,
dispatch: PropTypes.func.isRequired
}
function mapStateToProps(state) {
const { selectedSubreddit, postsBySubreddit } = state
const { isFetching, lastUpdated, items: posts } = postsBySubreddit[
selectedSubreddit
] || {
isFetching: true,
items: []
}
return {
selectedSubreddit,
posts,
isFetching,
lastUpdated
}
}
export default connect(mapStateToProps)(AsyncApp)
Picker.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Picker extends Component {
render() {
const { value, onChange, options } = this.props
return (
<span>
<h1>{value}</h1>
<select onChange={e => onChange(e.target.value)} value={value}>
{options.map(option => (
<option value={option} key={option}>
{option}
</option>
))}
</select>
</span>
)
}
}
Picker.propTypes = {
options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
}
Posts.js:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Posts extends Component {
render() {
return (
<ul>
{this.props.posts.map((post, i) => (
<li key={i}>{post.title}</li>
))}
</ul>
)
}
}
Posts.propTypes = {
posts: PropTypes.array.isRequired
}
Update:
import { push } from 'connected-react-router';
...
handleChange(nextSubreddit) {
this.props.dispatch(push('/reddit/' + nextSubreddit))
}
I placed this in the handleChange() method. When Picker changes, I push the state to the browser. However, when I go back and forward, the data does not change according to this url. I see the same data in every state.
We can handle this scenario using history property. We implement using listener of history and play with the location property which in turn provide pathname. It would be implement in componentDidUpdate. Everytime when back and forward button of browser clicked, the listener will called and service calls and state can be changed accordingly.
AsyncApp.js
// code here
import { history } from '../configureStore'
// code here
componentDidUpdate(prevProps) {
if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
const backBrowser = history.listen(location => {
console.log(location.pathname)
// code here
}
// code here
}
}
1.Can someone help me where i made a thing wrong?
2.the component i am mapping the state to its properties but i still get this
error"mapStateToProps is not defined"
this is the whole component below. the error reads "mapStateToProps not defined"
import React, {Component} from 'react';
import Icon from 'react-native-vector-icons/EvilIcons';
import { loadInitialPosts} from './actions';
import {connect } from 'react-redux';
import _ from 'lodash';
import {View, StyleSheet,FlatList} from 'react-native';
import PostItem from './PostItem';
import PostDetail from './PostDetail';
class PostsList extends Component {
componentWillMount() {
this.props.loadInitialPosts();
}
renderItem({item}){
return <PostItem posts = { item } />;
}
renderInitialView(){
if(this.props.postDetailView === true){
return(
<PostDetail />
);
}
else{
return(
<FlatList
data={this.props.posts}
renderItem={this.renderItem} />
)}
}
render(){
return(
<View style={styles.list}>
{this.renderInitialView()}
</View>
);
}
}
const mapStateToProps = state => {
const posts = _.map(state.posts, (val, id) =>
{
return { ...val, id};
});
return{
posts: posts,
postDetailView: state.postDetailView,
};
}
export default connect(mapStateToProps, { loadInitialPosts })(PostsList)
1.This is the action that dispatches the data
export const loadInitialPosts = () => {
return function(dispatch){
return axios.get(apiHost
+"/api/get_posts?
count=20")
.then((response) => {
dispatch({ type:
'INITIAL_POSTS_FETCH', payload:
response.data.posts});
}).catch((err) => {
console.log(err);
});
};
};
mapStateToProps sits outside of the class before export default connect(mapStateToProps)(SomeClass)
class SomeClass extends React.Component {
...
}
const mapStateToProps = state => {
const posts = _.map(state.posts, (val, id) => {
return { ...val,
id
};
});
return {
posts: posts,
postDetailView: state.postDetailView,
};
}
To eliminate the possibility of mapStateToProps being undefined, consider defining the mapStateToProps directly in the call to connect() like this:
class PostsList extends React.Component {
componentWillMount() {
this.props.loadInitialPosts();
}
renderItem({item}){
return <PostItem posts = { item } />;
}
renderInitialView(){
if(this.props.postDetailView === true){
return <PostDetail />;
}
else{
return <FlatList
data={this.props.posts}
renderItem={this.renderItem} />
}
}
render(){
return(<View style={styles.list}> {this.renderInitialView()} </View>);
}
}
/*
Avoid declaration of mapStateToProps object by defining this object
directly in the call to connect()
*/
export default connect((state => {
return {
posts : state.posts.map((val, id) => ({ ...val, id })),
postDetailView: state.postDetailView,
}
}), { loadInitialPosts })(PostsList)
I've asked a similar-ish question here before, however my code has changed quite a bit and I can not figure this out. I am certain it's an issue with what I am passing to my action/reducer. I would seriously appreciate it if someone could explain what I am doing wrong here. I really want to get this, just having a hard time with it.
actions.js
import { ADD_TODO, REMOVE_TODO } from '../constants/action-types';
export const addTodo = (todo) => (
{
type: ADD_TODO,
payload: todo
}
);
export const removeTodo = (id) => (
{
type: REMOVE_TODO,
payload: id
}
)
reducers.js
import { ADD_TODO, REMOVE_TODO, ADD_OPTIONS } from '../constants/action-types';
import uuidv1 from 'uuid';
const initialState = {
todos: []
};
const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [...state.todos,
{
title: action.payload.inputValue,
id: uuidv1(),
createdAt: Date(),
priority: '',
deadline: '',
isClicked: false
}]
}
case REMOVE_TODO:
return {
...state,
todos: [...state.todos.filter(todo => todo.id !== action.payload)]
}
case ADD_OPTIONS:
return {
...state,
todos: [...state.todos,
{
isClicked: false
}]
}
default:
return state;
}
}
export default rootReducer;
TodoList.js
import React, { Component } from 'react';
import TodoItem from './TodoItem';
import { removeTodo } from '../actions';
import { connect } from 'react-redux';
const mapDispatchToProps = dispatch => {
return {
removeTodo: id => dispatch(removeTodo(id))
};
};
const mapStateToProps = state => {
return {todos: [...state.todos]};
};
class List extends Component {
render() {
const mappedTodos = this.props.todos.map((todo, index) => (
<TodoItem
title={todo.title}
key={index}
removeTodo={this.props.removeTodo}
/>
));
return (
mappedTodos
);
}
}
const TodoList = connect(mapStateToProps, mapDispatchToProps) (List)
export default TodoList;
TodoItem.js
import React, { Component } from 'react';
import uuid from 'uuid';
import '../../css/Todo.css';
class TodoItem extends Component {
render() {
const todoId = uuid();
return (
<div id={todoId}>
{this.props.title}
<button onClick={this.props.removeTodo}>X</button>
</div>
);
}
}
export default TodoItem;
You need to wrap your remove handler in an expression that can be evaluated at "click time" and use the todo id from the closure:
class TodoItem extends Component {
render() {
const todoId = uuid();
return (
<div id={todoId}>
{this.props.title}
<button onClick={this.props.removeTodo}>X</button>
</div>
);
}
}
Should be something like...
class TodoItem extends Component {
render() {
const todoId = uuid();
return (
<div id={todoId}>
{this.props.title}
<button onClick={() => this.props.removeTodo(todoId)}>X</button>
</div>
);
}
}
Along the lines of what #The Dembinski was saying, it works when I change my TodoList component to look like this:
import React, { Component } from 'react';
import TodoItem from './TodoItem';
import { removeTodo } from '../actions';
import { connect } from 'react-redux';
const mapDispatchToProps = dispatch => {
return {
removeTodo: id => dispatch(removeTodo(id))
};
};
const mapStateToProps = state => {
return {todos: [...state.todos]};
};
class List extends Component {
render() {
const mappedTodos = this.props.todos.map((todo, index) => (
<TodoItem
title={todo.title}
key={index}
removeTodo={() => this.props.removeTodo(todo.id)}
/>
));
return (
mappedTodos
);
}
}
const TodoList = connect(mapStateToProps, mapDispatchToProps) (List)
export default TodoList;
Changing my removeTodo prop in the map here DID fix the issue and now deletes properly. However, if anyone could help me understand this better either by further discussion, or just by pointing my in the right direction as to what I should be researching. Would be greatly appreciated. I'm not after answers, I'm after learning.
I have searched around, all questions are something about How to pass props to {this.props.children}
But my situation is different,
I fill App with a initial data -- nodes, and map nodes to a TreeNodelist, and I want each TreeNode has the property of passed in node.
Pseudo code:
App.render:
{nodes.map(node =>
<TreeNode key={node.name} info={node} />
)}
TreeNode.render:
const { actions, nodes, info } = this.props
return (
<a>{info.name}</a>
);
Seems node not be passed in as info, log shows info is undefined.
warning.js?8a56:45 Warning: Failed propType: Required prop `info` was not specified in `TreeNode`. Check the render method of `Connect(TreeNode)`.
TreeNode.js?10ab:57 Uncaught TypeError: Cannot read property 'name' of undefined
below just a more complete code relate to this question(store and action is not much relation I think):
containers/App.js:
import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Footer from '../components/Footer';
import TreeNode from '../containers/TreeNode';
import Home from '../containers/Home';
import * as NodeActions from '../actions/NodeActions'
export default class App extends Component {
componentWillMount() {
// this will update the nodes on state
this.props.actions.getNodes();
}
render() {
const { nodes } = this.props
console.log(nodes)
return (
<div className="main-app-container">
<Home />
<div className="main-app-nav">Simple Redux Boilerplate</div>
<div>
{nodes.map(node =>
<TreeNode key={node.name} info={node} />
)}
</div>
<Footer />
</div>
);
}
}
function mapStateToProps(state) {
return {
nodes: state.opener.nodes
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(NodeActions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
containers/TreeNode.js
import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import classNames from 'classnames/bind'
import * as NodeActions from '../actions/NodeActions'
class TreeNode extends Component {
handleClick() {
this.setState({ open: !this.state.open })
if (this.state.open){
this.actions.getNodes()
}
}
render() {
const { actions, nodes, info } = this.props
if (nodes) {
const children =<div>{nodes.map(node => <TreeNode info={node} />)}</div>
} else {
const children = <div>no open</div>
}
return (
<div className={classNames('tree-node', { 'open':this.props.open})} onClick={ () => {this.handleClick()} }>
<a>{info.name}</a>
{children}
</div>
);
}
}
TreeNode.propTypes = {
info:PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
function mapStateToProps(state) {
return {
open: state.open,
info: state.info,
nodes: state.nodes
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(NodeActions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TreeNode);
reducers/TreeNodeReducer.js
import { OPEN_NODE, CLOSE_NODE, GET_NODES } from '../constants/NodeActionTypes';
const initialState = {
open: false,
nodes: [],
info: {}
}
const testNodes = [
{name:'t1',type:'t1'},
{name:'t2',type:'t2'},
{name:'t3',type:'t3'},
]
function getFileList() {
return {
nodes: testNodes
}
}
export default function opener(state = initialState, action) {
switch (action.type) {
case OPEN_NODE:
var {nodes} = getFileList()
return {
...state,
open:true,
nodes:nodes
};
case CLOSE_NODE:
return {
...state,
open:false
};
case GET_NODES:
var {nodes} = getFileList()
return {
...state,
nodes:nodes
};
default:
return state;
}
}
For complete code, can see my github https://github.com/eromoe/simple-redux-boilerplate
This error make me very confuse. The sulotion I see are a parent already have some children, then feed props to them by using react.Children, and them don't use redux.
When looping on nodes values, you call TreeNode and give the property info: that is good!
But when your component is rendered, this function is called:
function mapStateToProps(state) {
return {
open: state.open,
info: state.info,
nodes: state.nodes
};
}
As you can see, the prop info will be overriden with the value in state.info. state.info value is undefined I think. So React warns you that TreeNode requires this value. This warning comes from your component configuration:
TreeNode.propTypes = {
info:PropTypes.object.isRequired
}
Why state.info is undefined? I think you doesn't call it as it should. You should call state['reducerNameSavedWhenCreatingReduxStore].infoto retreive{}`.
You shouldn't fill ThreeNode through both props & connect().
It's because you are rendering a Redux connected component from within a parent Redux connected component and trying to pass props into it as state.
Why does TreeNode.js need to be connected to Redux? Props/Actions should be passed uni-directionally with only the top level component connected to state and all child components being essentially dumb components.
TreeNode should look similar to this:
class TreeNode extends Component {
handleClick() {
this.setState({ open: !this.state.open })
if (this.state.open){
this.props.actions.getNodes();
}
}
render() {
const { nodes, info } = this.props
if (nodes) {
const children =<div>{nodes.map(node => <TreeNode info={node} />)}</div>
} else {
const children = <div>no open</div>
}
return (
<div className={classNames('tree-node', { 'open':this.props.open})} onClick={ () => {this.handleClick()} }>
<a>{info.name}</a>
{children}
<div>{nodes.map(node => <TreeNode info={node} />)}</div>
</div>
);
}
}
TreeNode.propTypes = {
info: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
export default class TreeNode;
and the parent component would render TreeNode like this, passing the props in to the component:
<div>
{nodes.map(node =>
<TreeNode key={node.name} info={node} actions={this.props.actions} />
)}
</div>