I am using react and redux.
I have a Container component defined as so:
import { connect } from 'react-redux';
import {addTag} from 'actions';
import ExpenseTagsControl from './expense_tags_control'
const mapStateToProps = (state, own_props={selected_tags:[]}) => {
return {
tags_list: state.tags.tags_list
};
};
const mapDispatchToProps = (dispatch) => {
return {
addTag: (tag_name) => {
dispatch(addTag(tag_name))
}
};
};
const AddExpenseTagsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ExpenseTagsControl);
export default AddExpenseTagsContainer;
The container wraps a presentational component which is defined as so:
// expense_tags_control.js
import React, {Component, PropTypes} from 'react';
import ChipInput from 'material-ui-chip-input';
import Chip from 'material-ui/Chip';
import Avatar from 'material-ui/Avatar';
import Tag from 'common/svg_icons/tag';
import AutoComplete from 'material-ui/AutoComplete'
import _ from 'underscore';
class ExpenseTagsControl extends React.Component {
constructor(props) {
super(props);
this.state = {
chips: []
};
};
handleAdd(chip) {
// If the chip does not already exist, add it. the id here will be a dummy value that is not there in the tags_list
if (!(_.contains( _.map(this.props.tags_list, (tag) => tag.id), chip.id))) {
this.props.addTag(chip.name);
}
// This is wrong.
this.setState({
chips: [...this.state.chips, chip]
});
};
handleDelete(chip) {
this.setState({
chips: this.state.chips.filter((c) => c !== deletedChip)
});
};
chipRenderer({ text, value, isFocused, isDisabled, handleClick, handleRequestDelete }, key) {
const style = {
margin: '8px 8px 0 0',
float: 'left',
pointerEvents: isDisabled ? 'none' : undefined
};
return (
<Chip key={key} style={style} onTouchTap={handleClick} onRequestDelete={handleRequestDelete}>
<Avatar size={24} icon={<Tag />} />
{text}
</Chip>
);
};
render() {
return (
<ChipInput
hintText="Tags"
value={this.state.chips}
onRequestAdd={(chip) => this.handleAdd(chip)}
onRequestDelete={(deletedChip) => this.handleDelete(deletedChip)}
fullWidth={true}
dataSourceConfig={{ text: 'name', value: 'id' }}
dataSource={this.props.tags_list}
chipRenderer={this.chipRenderer}
openOnFocus={false}
filter={AutoComplete.fuzzyFilter}
onRequestDelete={console.log("Deleted")}
/>);
};
};
ExpenseTagsControl.PropTypes = {
tags_list: PropTypes.array.isRequired,
addTag: PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired
};
export default ExpenseTagsControl;
The presentational component above, maintains a state, which indicates the chips that have been selected.
The ChipInput component allows you to select chips which are objects with an id, and a name, defined from a pre-existing data source. The component also allows you to add a new chip by typing in the name. If the typed in name does not exist in the data source, it is added to the data source.
My Problem
The id of the newly added chip is assigned once the addTag() action is dispatched. How do I get the value of the result of the action that was just dispatched?
I thought about working around this by maintaining the state of the ChipInput in the global state, and manipulate the global state upon dispatching the addTag() action. But that feels like too much overhead.
If what I understand is correct, you might want something like this:
class ExpenseTagsControl extends React.Component {
// ...
/*
* assuming your reducers are working fine and 'addTag'
* has updated global 'state.tags.tags_list'
*/
componentWillReceiveProps(nextProps) {
this.setState({ chips: this.nextProps.tags_list });
}
// ...
}
NB: You might need to optimize calling setState inside componentWillReceiveProps based on some conditions to avoid unnecessary re-render.
From what I understand, the OP's problem is how to dispatch an action to modify the redux store and at the same time update the component's local state.
Edit: added a working example
const initialState = {
tags: ['hello', 'hi', 'howdy']
}
function reducer(state = {}, action) {
switch (action.type) {
case 'ADD_TAG':
return {
...state,
tags: [
...state.tags,
action.payload.tag
]
}
default:
return state;
}
}
const store = Redux.createStore(reducer, initialState);
const addTag = (tag) => ({
type: 'ADD_TAG',
payload: {
tag
}
})
class Chips extends React.Component {
constructor(props) {
super(props);
this.chipToAdd = false;
this.state = {
chips: []
}
this.handleAdd = this.handleAdd.bind(this);
}
componentWillReceiveProps(nextProps) {
console.log(this.chipToAdd);
if (this.chipToAdd) {
this.setState({
chips: [...this.state.chips, this.chipToAdd]
}, (this.chipToAdd = false));
}
}
handleAdd(chip) {
if (this.props.tags.filter(tag => tag === chip).length === 0) {
this.chipToAdd = chip;
this.props.addTag(chip);
} else {
if (this.state.chips.filter(existingChip => existingChip === chip).length === 0) {
this.setState({
chips: [...this.state.chips, chip]
});
}
}
}
render() {
return <div >
< h3 > Tags added in component 's chip state</h3>
<ul>
{this.state.chips.map((chip, index) => <li key={index}>{chip}</li>)}
</ul>
<hr />
<h3>Tags in Redux Store</h3>
{this.props.tags.map(
(tag, index) => <li key={index}>
{tag} <button onClick={() => this.handleAdd(tag)}>Add</button>
</li>
)}
<button onClick={() => this.handleAdd('
new tag - ' + Math.floor((Math.random() * 100) + 1))}>Add a chip with new tag</button>
</div>
}
}
const mapStateToProps = ({ tags = [] }) => ({ tags });
const ConnectedChips = ReactRedux.connect(mapStateToProps, { addTag })(Chips);
class App extends React.Component {
render() {
return <div>
<h1>React/Redux Demo</h1>
<ConnectedChips />
</div>
}
}
const Provider = ReactRedux.Provider;
ReactDOM.render(
<Provider store={store}><App /></Provider>,
document.getElementById('
root ')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://unpkg.com/redux#3.6.0/dist/redux.min.js"></script>
<script src="https://unpkg.com/react-redux#4.4.6/dist/react-redux.min.js"></script>
<div id="root"></div>
Related
I am trying to create a sort button which when clicked will sort me menu cards alphabetically. My question is how should I have the sort function coded in the Reducer and Actions? I added pseudo-code for sorting in the Reducer as well. When I click the button I am getting "(TypeError): state.slice is not a function".
Edit:
Added my button component and main Container.
Actions:
export const sortMenus = () => {
return dispatch => {
dispatch({ type: "LOADING_MENUS" });
fetch(`/api/menus`)
.then(res => res.json())
.then(responseJSON => {
dispatch({ type: "SORT_MENUS", cards: responseJSON });
});
};
};
Reducer:
export default function MenusReducer(
state = {
cards: [],
loading: false
},
action
) {
switch (action.type) {
case "LOADING_MENUS":
return {
...state
};
case "ADD_MENUS":
return {
...state,
cards: action.cards
};
case "SORT_MENUS":
return state.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
default:
return state;
}
}
Button Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { sortMenus } from ".././actions/dataActions";
import Row from "react-bootstrap/Row";
import Container from "react-bootstrap/Container";
class SortButton extends Component {
constructor() {
super();
this.state = { menus: [] };
}
handleMenuSort = e => {
this.props.sortMenus()
};
render() {
return (
<Container>
<Row>
<div>
<button id="sort-button" title="Sort Menus" onClick= {this.handleMenuSort}>Sort Menus</button>
</div>
</Row>
</Container>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
sortMenus: params => dispatch(sortMenus(params)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(SortButton)
Container:
class MainContainer extends Component {
displayCards = () => {
switch(this.props.path) {
case "menus":
return (this.props.menus.cards.map(card => (
<NavLink style={{ color: "black" }} to={`/menus/${card.id}`} key={card.id}><MenuCard view={this.props.displayObject} info={card} /></NavLink>
)));
default:
return (<div>Empty</div>)
}
};
render() {
return (
<CardColumns>
{this.displayCards()}
</CardColumns>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
displayObject: (id, category, type) => dispatch(displayObject(id, category, type)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(MainContainer)
Your state is an object, not an array. You likely mean to sort the stored cards array.
state.cards.slice(... instead of state.slice(...
case "SORT_MENUS":
return state.cards.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
Side note: You may also want to clear/set your loading state upon successful data fetching. ;)
EDIT
You are mapping undefined state within mapStateToProps, then mapping over it in the component. Change mapStateToProps to access the correct defined property.
const mapStateToProps = state => ({
cards: state.cards,
});
Then you can iterate over the new cards prop.
case "menus":
return (this.props.cards.map(card => (
<NavLink
style={{ color: "black" }}
to={`/menus/${card.id}`}
key={card.id}
>
<MenuCard view={this.props.displayObject} info={card} />
</NavLink>
)));
You can simply store the fetched menu in application state.
You can have standalone action say SORT_MENU_BY_ALPHABET.
You can simply dispatch this action on button handler as well as on Ajax success. this dispatch may not have any payload associated.
hope it helps.
in reducer you defined state as object and you're trying to do array operation on it. state.slice().
slice is a function available for arrays. so its throwing error.
you should be doing
state.cards.slice().sort((a,b)=> a-b)
I would like to understand the behavior of react component constructor. Let suppose I have three components - PageComponent, ListComponent, ItemComponent. My pseudo-code structure is:
PageComponent (get data from redux, fetch data)
ListComponent (obtains data as props, in loop (map) renders list of ItemComponents)
ItemComponent (obtains item data as props, renders item, manipulate data)
Logic:
- when data in ItemComponent changes, changes are stored in REDUX and this change caused list re-rendering.
Use-case 1:
- PageComponent renders ListComponent and ListComponent renders list of ItemComponets
- when REDUX listItem data chages, PageComponent is updated, ListComponent is updated and ItemComponent CONSTRUCTOR is called (its local state is reset)
Use-case 2:
- PageComponent renders only LIST (using map loop) of ItemComponents.
- when REDUX listItem data chages, PageComponent is updated ItemComponent CONSTRUCTOR is NOT called (component is "only" updated) (and its local state is NOT reset)
Why there is a different behavior in these examples?
Source code:
PageComponent:
import React from 'react'
...
class UsersPage extends React.Component {
constructor(props) {
super(props)
props.actions.getUsers();
}
render() {
const {users} = this.props
return (
<Main>
{/* // NO ITEM CONSTRUCTOR IS CALLED
users.data.items.map((item, index) => {
return <ListItemComponent
data={item}
itemMethods={{
getItem: (data) => this.props.actions.getUser(data),
onEdit: (data) => this.props.actions.updateUser(data),
onDelete: (data) => this.props.actions.deleteUser(data),
validation: (data) => validateInput(this.props.strings, data)
}}
key={index}
/>
})*/
}
{ // ITEM CONSTRUCTOR IS CALLED
<ListComponent
loading={users.isFetching}
data={users.data}
methods={{
getItem: (data) => this.props.actions.getUser(data),
onEdit: (data) => this.props.actions.updateUser(data),
onDelete: (data) => this.props.actions.deleteUser(data),
validation: (data) => validateInput(this.props.strings, data)
}}
/>}
</Main>
);
}
}
UsersPage.propTypes = {
users: PropTypes.object.isRequired,
strings: PropTypes.object.isRequired,
}
function mapStateToProps(state) {
return {
users: state.users,
strings: state.strings.data || {},
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getUsers,
getUser,
addUser,
updateUser,
deleteUser,
}, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withAlert(UsersPage));
ListComponent:
import React from 'react'
...
class ListComponent extends React.Component {
getList() {
return <div className="list-outer">
<Row>
{
items.map((item, index) => {
return <ListItemComponent
data={item}
itemMethods={methods}
key={index}
/>
})
}
</Row>
</div>
}
render() {
const {loading} = this.props
return (
<div className="list-wrapper">
{
loading ? <Spinner visible={true}/>
:
this.getList()
}
</div>
)
}
}
ListComponent.propTypes = {
loading: PropTypes.bool.isRequired,
data: PropTypes.object.isRequired,
methods: PropTypes.object.isRequired,
}
export default ListComponent
ListItemComponent:
import React from 'react'
...
class ListItemComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
editMode: false,
}
}
toggleEditMode(){
const editMode = this.state.editMode
this.setState({editMode: !editMode})
}
onEdit(id) {
const itemMethods = this.props.itemMethods
this.toggleEditMode()
itemMethods.getItem({id: id})
}
onDelete(item) {
//...
}
getFields(rowData, index) {
return <div key={index}>
{
rowData.map((itm, idx) => {
return <div key={idx}>{itm.label}: {itm.value}</div>
})
}
</div>
}
render() {
const editMode = this.state.editMode
const {data, itemMethods, strings} = this.props
return (
editMode ?
<Form
id={data.id}
onSubmit={(data) => itemMethods.onEdit(data)}
validation={(data) => itemMethods.validation(data)}
onCloseForm={() => this.toggleEditMode()}
/>
:
<Col xs={12}>
<div>
<div
{this.getFields(data)}
</div>
<div className="controls">
<button
className="btn btn-theme inverse danger"
onClick={() => this.onDelete(data)}
>{strings.delete}</button>
<button
onClick={() => this.onEdit(data.id)}
className="btn btn-theme" type="button"
>
{strings.edit}
</button>
</div>
</div>
</Col>
)
}
}
ListItemComponent .propTypes = {
strings: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
itemMethods: PropTypes.object.isRequired,
}
function mapStateToProps(state) {
return {
strings: state.strings.data || {}
};
}
export default connect(
mapStateToProps,
null,
)(ListItemComponent )
Ensure each ItemComponent has a key prop set. When React renders your list of items, it needs to know how to identify each element and React leaves it up to you to do this. If you omit the key prop, React will destroy and re-create your list upon each re-render, which means calling the component constructor.
If you provide the exact code you're using, we can better point out where your issue is coming from.
You can read more about lists and keys here.
SOLVED
It was cause by ListComponent and the loading prop that was placed as condion in render function. When item was edited, prop loading was set to true, spinner became visible AND it was the only element in ListComponent and therefore the list items were unmounted
I'm pretty new to React and Redux and very new to react-dnd, and I think I'm doing something wildly incorrect here. Although there are other similar posts out there I can't quite find a solution in them.
I'm working on a Kanban board app that is somewhat based on the one found at https://survivejs.com/react/implementing-kanban/drag-and-drop/ though that version uses Alt.js and I'm using Redux.
The problem: when dragging a component, the action function is called but the case in the reducer (MOVE_TICKET) is not. This seems to be the case regardless of the content of the action function.
I linked the action to a click event and in this instance the action and reducer worked as expected. This leads me to think that it must be a problem with the way I've set up the Ticket component with the dnd functions.
Ticket.js:
import React from "react"
import {compose} from 'redux';
import { DragSource, DropTarget } from 'react-dnd';
import ItemTypes from '../constants/ItemTypes';
import { moveTicket } from "../actions/ticketsActions"
const Ticket = ({
connectDragSource, connectDropTarget, isDragging, isOver, onMove, id, children, ...props
}) => {
return compose (connectDragSource, connectDropTarget)(
<div style={{
opacity: isDragging || isOver ? 0 : 1
}} { ...props } className = 'ticket'>
<h3 className = 'summary'> { props.summary } </h3>
<span className = 'projectName'> { props.projectName }</span>
<span className = 'assignee'> { props.assignee } </span>
<span className = 'priority'> { props.priority } </span>
</div>
);
};
const ticketSource = {
beginDrag(props) {
return {
id: props.id,
status: props.status
};
}
};
const ticketTarget = {
hover(targetProps, monitor) {
const targetId = targetProps.id;
const sourceProps = monitor.getItem();
const sourceId = sourceProps.id;
const sourceCol = sourceProps.status;
const targetCol = targetProps.status;
if(sourceId !== targetId) {
targetProps.onMove({sourceId, targetId, sourceCol, targetCol});
}
}
};
export default compose(
DragSource(ItemTypes.TICKET, ticketSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
})),
DropTarget(ItemTypes.TICKET, ticketTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver()
}))
)(Ticket)
ticketsReducer.js:
export default function reducer(state={
tickets: [],
fetching: false,
fetched: false,
error: null,
}, action) {
switch (action.type) {
case "MOVE_TICKET": {
return [{...state, tickets: action.payload}]
}
}
return state
}
ticketsActions.js
import store from '../store';
export function moveTicket({sourceId, targetId, sourceCol, targetCol}) {
const columns = Object.assign({}, store.getState().tickets.tickets)
const sourceList = columns[sourceCol];
const targetList = columns[targetCol];
const sourceTicketIndex = sourceList.findIndex(ticket => ticket.id == sourceId);
const targetTicketIndex = targetList.findIndex(ticket => ticket.id == targetId);
if(sourceCol === targetCol){
var arrayClone = sourceList.slice();
arrayClone.splice(sourceTicketIndex, 1);
arrayClone.splice(targetTicketIndex, 0, sourceList[sourceTicketIndex]);
columns[sourceCol] = arrayClone;
}
return function(dispatch){
dispatch({type: "MOVE_TICKET", payload: columns});
}
}
Column.js (where each Ticket component is rendered)
import React from "react"
import uuid from "uuid"
import { connect } from "react-redux"
import ColumnsContainer from "./ColumnsContainer"
import Ticket from "./ticket"
import { moveTicket } from "../actions/ticketsActions"
#connect((store) => {
return {
columns: store.columns.columns
};
})
export default class Column extends React.Component {
console(){
console.log(this)
}
render(){
const tickets = this.props.tickets.map((ticket, id) =>
<Ticket
key = {uuid.v4()}
id={ticket.id}
summary = { ticket.summary }
assignee = { ticket.assignee }
priority = { ticket.priority }
projectName = { ticket.displayName }
onMove={ moveTicket }
status= { ticket.status }
/>
)
return(
<div key = {uuid.v4()} className = { this.props.className }>
<h2 key = {uuid.v4()}>{ this.props.title }</h2>
<ul key = {uuid.v4()}>{ tickets }</ul>
</div>
)
}
}
If anyone can see where I'm going wrong I could really use some assistance.
You are not connecting the moveTicket action to redux's dispatcher.
You'll have to do something like:
#connect((store) => {
return {
columns: store.columns.columns
};
}, {moveTicket})
export default class Column extends React.Component {
// ...
// use this.props.moveTicket instead of moveTicket
The second parameter to connect is called mapDispatchToProps, which will do the dispatch(actionFn) for you.
You might want to name the bound action differently, e.g.
#connect((store) => {
return {
columns: store.columns.columns
};
}, {connectedMoveTicket: moveTicket})
// then use this.props.connectedMoveTicket
I use react and redux in my web app. It's the simple app which has 4 components, one reducer and 3 actions. After I add a new entry to list, react renders component of list (the listItem), then re-renders the whole app. What is the cause of re-rendering whole app after rendering one component?
Updated:
App container:
class App extends Component {
static propTypes = {
groups: PropTypes.array.isRequired,
actions: PropTypes.object.isRequired
};
render() {
return (<div>
<Header addGroup={this.props.actions.addGroup} />
<List groups={this.props.groups} />
</div>
);
}
}
function mapStateToProps(state) {
return { groups: state.groups };
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(AppActions, dispatch) };
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Reduser:
export default function groupDiseases(state = initialState, action){
switch (action.type) {
case ADD_GROUP:
return [
{
id: '',
name: action.name
},
...state
];
case DELETE_GROUP:
return state.filter(group =>
group.id !== action.id
);
case EDIT_GROUP:
return state.map(group => (group.id === action.id ? { id: action.id, name: action.name } : group));
default:
return state;
}
}
Components:
export default class Add extends Component {
static propTypes = {
addGroup: PropTypes.func.isRequired
}
componentDidMount() {
this.textInput.focus();
}
handleAdd = () => {
const name = this.textInput.value.trim();
if (name.length !== 0) {
this.props.addGroup(name);
this.textInput.value = '';
}
}
render() {
return (
<form className="add_form">
<input
type="text"
className="add__name"
defaultValue=""
ref={(input) => this.textInput = input}
placeholder="Name" />
<button
className="add__btn"
ref="add_button"
onClick={this.handleAdd}>
Add
</button>
</form>
);
}
}
export default class ListGroups extends Component {
static propTypes = {
groups: PropTypes.array.isRequired
};
render() {
let data = this.props.groups;
let groupTemplate = <div> Группы отсутствуют. </div>;
if (data.length) {
groupTemplate = data.map((item, index) => {
return (
<div key={index}>
<Item item={item} />
</div>
);
});
}
return (
<div className="groups">
{groupTemplate}
<strong
className={'group__count ' + (data.length > 0 ? '' : 'none')}>
Всего групп: {data.length}
</strong>
</div>
);
}
}
It's likely due to the fact that you are letting the <form> continue its default behavior, which is to submit to a targeted action. Take a look at the w3c spec for buttons:
http://w3c.github.io/html-reference/button.html
Specifically, a button with no type attribute will default to submit.
So your button is telling the form to submit, with the target being the current page since none is provided. In your handleAdd method, you can do something like:
handleAdd = (event) => {
event.preventDefault(); // prevent default form submission behavior
const name = this.textInput.value.trim();
if (name.length !== 0) {
this.props.addGroup(name);
this.textInput.value = '';
}
}
Or you can modify your button to have type="button".
I am slowly learning React and also learning to implement it with Redux. But I seem to have hit a road block. So this is what I have so far.
/index.jsx
import './main.css'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App.jsx'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import ShoppingList from './reducers/reducer'
let store = createStore(ShoppingList)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
)
/actions/items.js
import uuid from 'node-uuid'
export const CREATE_ITEM = 'CREATE_ITEM'
export function createItem(item) {
return {
type: CREATE_ITEM,
item: {
id: uuid.v4(),
item,
checked: false
}
}
}
/reducers/reducer.js
import * as types from '../actions/items'
import uuid from 'node-uuid'
const initialState = []
const items = (state = initialState, action) => {
switch (action.type) {
case types.CREATE_ITEM:
return {
id: uuid.v4(),
...item
}
default:
return state;
}
}
export default items
/reducers/index.js
UPDATE:
import { combineReducers } from 'redux'
import items from './reducer'
const ShoppingList = combineReducers({
items
})
export default ShoppingList
/components/Item.jsx
import React from 'react';
import uuid from 'node-uuid'
export default class Item extends React.Component {
constructor(props) {
super(props);
this.state = {
isEditing: false
}
}
render() {
if(this.state.isEditing) {
return this.renderEdit();
}
return this.renderItem();
}
renderEdit = () => {
return (
<input type="text"
ref={(event) =>
(event ? event.selectionStart = this.props.text.length : null)
}
autoFocus={true}
defaultValue={this.props.text}
onBlur={this.finishEdit}
onKeyPress={this.checkEnter}
/>
)
};
renderDelete = () => {
return <button onClick={this.props.onDelete}>x</button>;
};
renderItem = () => {
const onDelete = this.props.onDelete;
return (
<div onClick={this.edit}>
<span>{this.props.text}</span>
{onDelete ? this.renderDelete() : null }
</div>
);
};
edit = () => {
this.setState({
isEditing: true
});
};
checkEnter = (e) => {
if(e.key === 'Enter') {
this.finishEdit(e);
}
};
finishEdit = (e) => {
const value = e.target.value;
if(this.props.onEdit) {
this.props.onEdit(value);
this.setState({
isEditing: false
});
}
};
}
/components/Items.jsx
import React from 'react';
import Item from './Item.jsx';
export default ({items, onEdit, onDelete}) => {
return (
<ul>{items.map(item =>
<li key={item.id}>
<Item
text={item.text}
onEdit={onEdit.bind(null, item.id)}
onDelete={onDelete.bind(null, item.id)}
/>
</li>
)}</ul>
);
}
// UPDATE: http://redux.js.org/docs/basics/UsageWithReact.html
// Is this necessary?
const mapStateToProps = (state) => {
return {
state
}
}
Items = connect(
mapStateToPros
)(Items) // `SyntaxError app/components/Items.jsx: "Items" is read-only`
//////////////////////////////////////
// Also tried it this way.
//////////////////////////////////////
Items = connect()(Items)
export default Items // same error as above.
Tried this as well
export default connect(
state => ({
items: store.items
})
)(Items) // `Uncaught TypeError: Cannot read property 'items' of undefined`
UPDATE:
After many attempts #hedgerh in Gitter pointed out that it should be state.items instead. so the solution was
export default connect(
state => ({
items: state.items
})
)(Items)
credits to #azium as well.
/components/App.jsx
export default class App extends React.Component {
render() {
return (
<div>
<button onClick={this.addItem}>+</button>
<Items />
</div>
);
}
}
What am I missing here in order to implement it correctly? Right now it breaks saying that Uncaught TypeError: Cannot read property 'map' of undefined in Items.jsx. I guess it makes sense since it doesn't seem to be hooked up correctly. This is the first part of the app, where the second will allow an user to create a many lists, and these lists having many items. I will probably have to extract the methods from Item.jsx since the List.jsx will do pretty much the same thing. Thanks
You're missing connect. That's how stuff gets from your store to your components. Read the containers section from the docs http://redux.js.org/docs/basics/UsageWithReact.html
import React from 'react'
import Item from './Item.jsx'
import { connect } from 'react-redux'
let Items = ({items, onEdit, onDelete}) => {
return (
<ul>{items.map(item =>
<li key={item.id}>
<Item
text={item.text}
onEdit={onEdit.bind(null, item.id)}
onDelete={onDelete.bind(null, item.id)}
/>
</li>
})
</ul>
)
}
export default connect(
state => ({
items: state.items
})
)(Items)
Also you seem to be expecting onEdit and onDelete functions passed from a parent but you're not doing that so those functions will be undefined.