How to reuse reducer with same action using redux-subspace - reactjs

I'm building a small app using React, semantic-ui-react, redux-subspace.
I have many different tables and when the user clicks on one of the cells, the value supposed to come out on the console but the result is undefined when it clicked. I'm trying to reuse reducer. Same action with different instances.
I appreciate any comments that guide me to right direction.
PartA.js
This component renders Tables and wrapped with <SubspaceProvider>.
<Segment inverted color='black'>
<h1>Age </h1>
{ this.state.toggle ?
<SubspaceProvider mapState={state => state.withSpouseAge} namespace="withSpouseAge">
<TableForm
headers={spouse_ageHeaders}
rows={spouse_ageData}
namespace={'withSpouseAge'}
/>
</SubspaceProvider> :
<SubspaceProvider mapState={state => state.withoutSpouseAge} namespace="withoutSpouseAge">
<TableForm
headers={withoutSpouse_ageHeader}
rows={withoutSpouse_ageData}
namespace={'withoutSpouseAge'}
/>
</SubspaceProvider> }
TableForm.js
This component return Table with the Data and this is where I want to implement onClick method.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Table } from 'semantic-ui-react';
import { select } from '../actions';
const shortid = require('shortid');
class TableForm extends Component {
constructor(props){
super(props);
this.state = {
activeIndex: 0,
}
this.handleOnClick = this.handleOnClick.bind(this);
this.isCellActive = this.isCellActive.bind(this);
};
isCellActive(index) {
this.setState({ activeIndex: index });
}
handleOnClick(index, point) {
this.isCellActive(index);
this.props.onSelect(point);
};
tableForm = ({ headers, rows }) => {
const customRenderRow = ({ factor, point, point2 }, index ) => ({
key: shortid.generate(),
cells: [
<Table.Cell content={factor || 'N/A'} />,
<Table.Cell
content={point}
active={index === this.state.activeIndex}
textAlign={'center'}
selectable
onClick={() => this.handleOnClick(index, point)}
/>,
<Table.Cell
content={point2}
textAlign={'center'}
selectable
/>
],
});
return (
<Table
size='large'
padded
striped
celled
verticalAlign={'middle'}
headerRow={this.props.headers}
renderBodyRow={customRenderRow}
tableData={this.props.rows}
/>
)
};
render() {
console.log(this.props.withSpouseAgePoint);
const { headers, rows } = this.props;
return (
<div>
{this.tableForm(headers, rows)}
</div>
);
}
};
const mapDispatchToProps = (dispatch) => {
return {
onSelect: (point) => {dispatch(select(point))},
}
}
const mapStateToProps = state => {
return {
withSpouseAgePoint: state.withSpouseAge,
withSpouseLoePoint: state.withSpouseLoe,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TableForm);
Action
import {
SELECT,
} from './types';
export const select = (points) => ({
type: 'SELECT',
points,
});
Reducer.js
import { SELECT } from '../actions/types';
const INITIAL_STATE = {
point: 0,
};
const selectionReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'SELECT':
return { ...state, point: state.point + action.points };
default:
return state;
}
};
export default selectionReducer;
Reducer index.js
import { createStore, combineReducers } from 'redux';
import { subspace, namespaced } from 'redux-subspace';
import selectionReducer from './selectionReducer';
import toggleReducer from './toggleReducer';
const reducers = combineReducers({
withSpouseAge: namespaced('withSpouseAge')(selectionReducer),
withSpouseLoe: namespaced('withSpouseLoe')(selectionReducer),
withSpouseOlp: namespaced('withSpouseOlp')(selectionReducer),
withSpouseOlp2: namespaced('withSpouseOlp2')(selectionReducer),
withSpouseExp: namespaced('withSpouseExp')(selectionReducer),
withoutSpouseAge: namespaced('withoutSpouseAge')(selectionReducer),
withoutSpouseLoe: namespaced('withoutSpouseLoe')(selectionReducer),
withoutSpouseOlp: namespaced('withoutSpouseOlp')(selectionReducer),
withoutSpouseOlp2: namespaced('withoutSpouseOlp2')(selectionReducer),
withoutSpouseExp: namespaced('withoutSpouseExp')(selectionReducer),
toggle: toggleReducer,
});
Update
I added below TableForm component
const mapDispatchToProps = (dispatch) => {
return {
onSelect: (point) => {dispatch(select(point))},
}
}
const mapStateToProps = state => {
return {
withSpouseAgePoint: state.withSpouseAge,
withSpouseLoePoint: state.withSpouseLoe,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TableForm);
implement this.props.onSelect(point) on handleOnClick. It still shows me the same result undefined. I checked store states by getState(). consloe.log. I think my implementation of redux-subspace is wrong. I uploaded whole TableForm component and also updated reducer. Please help me out!
update 2
I replaced mapStateToProps and it worked like a magic. Thank you again #JustinTRoss.
but there is another problem, all the states are coming out with the same value when I clicked on the cell.
. my plan is each state has their own value stored.
const mapStateToProps = state => {
return {
withSpouseAgePoint: state,
withoutSpouseAge: state,
}
}

You have already namespaced your component to withSpouseAge and mapped state to state.withSpouseAge in your SubspaceProvider. Thus, you're calling the equivalent of state.withSpouseAge.withSpouseAge (undefined).
Another potential issue is the signature with which you are calling connect. From the snippet you provided, there's no way to be sure of the value of 'select'. Typically, connect is called with 2 functions, often named mapStateToProps and mapDispatchToProps. You are calling connect with a function and an object. Here's an example from http://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/#connect :
import {connect} from 'react-redux'
const TodoItem = ({todo, destroyTodo}) => {
return (
<div>
{todo.text}
<span onClick={destroyTodo}> x </span>
</div>
)
}
const mapStateToProps = state => {
return {
todo : state.todos[0]
}
}
const mapDispatchToProps = dispatch => {
return {
destroyTodo : () => dispatch({
type : 'DESTROY_TODO'
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoItem)
Additionally, there's one other issue, although it isn't affecting you yet: You're calling this.tableForm with 2 arguments (headers and rows), while you defined the this.tableForm function to take a single argument and destructure out 'headers' and 'rows' properties.

Related

How to fix problem with React-Redux(mapDispatchToProps() in Connect() must return a plain object. Instead received undefined.)

I ported my application to redux-thunk and the error started to appear in the console(
mapDispatchToProps() in Connect(UsersContainer) must return a plain object. Instead received undefined.).
But with the appearance of this error, nothing has changed. How to fix it?
Reducer:
import {getTeamApi} from "./api";
let date = {
teamDate: []
};
const realtorsDate = (state = date, action) => {
switch (action.type) {
case "GetTeam":
return {...state, teamDate: [...state.teamDate, ...action.team]};
default:
return state
}
}
export let GetTeam = (team) => ({
type: "GetTeam",
team
})
export default realtorsDate;
export const getTeamThunks = () => {
return (dispatch) => {
getTeamApi.then(response => {
dispatch(GetTeam(response.items));
});
}
}
Container component:
import {connect} from "react-redux";
import {getTeamThunks} from "../../store/realtorsDate";
import ScrollableAnchor from "react-scrollable-anchor";
import MainFourth from "./main-fourth";
import Photo from "../../Images/pivo-3.jpg";
import React from "react";
class UsersContainer extends React.Component {
render() {
return (
<section>
<ScrollableAnchor id={"team"}>
<h2>Наша команда</h2>
</ScrollableAnchor>
<div className="MainFourth">
{this.props.realtorsDate.map((el, i) => (
<MainFourth key={i} el={el} Photo={Photo}></MainFourth>))}
</div>
</section>
);
}
}
let MapStateToProps = (state) => {
return {
realtorsDate: state.realtorsDate.teamDate
}
}
let FourthContainerBlock = connect(MapStateToProps, getTeamThunks)(UsersContainer)
export default FourthContainerBlock
Component:
import React from "react";
import "./../../css/App.css";
import {FontAwesomeIcon} from "#fortawesome/react-fontawesome";
import {faAt, faMobileAlt} from "#fortawesome/free-solid-svg-icons";
class MainFourth extends React.Component {
render() {
return (
<div className="team" key={this.props.i}>
<div className="about-team">
<h2 className="team-name">
{this.props.el.SecondName}
{this.props.el.Name}
</h2>
<h2 className="team-position">{this.props.el.Position}</h2>
<p><FontAwesomeIcon icon={faMobileAlt}></FontAwesomeIcon> : {this.props.el.Phone}</p>
<p><FontAwesomeIcon icon={faAt}></FontAwesomeIcon> : {this.props.el.Mail}</p>
</div>
<img className="TeamsPhoto" src={this.props.Photo} alt="" />
</div>
);
}
}
export default MainFourth;
Ty all
The error is actually pretty straight forward, getTeamThunks should return a js object and that should be synchronous. You can do asynchronous things in the action creators.
The second argument to connect is simply used to map the dispatch to props(the reason why most tend to name it as such).
For example, mapDispatchToProps can look like this:
const mapDispatchToProps = (dispatch) => {
return {
getTeamThunks: () => dispatch(actions.getTeamThunksData())
}
}
let FourthContainerBlock = connect(MapStateToProps, mapDispatchToProps)(UsersContainer)
Now the getTeamthunksData can be written like this:
export const getTeamThunksData = () => {
return (dispatch) => {
getTeamApi.then(response => {
dispatch(GetTeam(response.items));
});
}
}
In your component, you can dispatch it by using this.props.getTeamThunks(). Where you execute it depends on your requirements.

Sorting Data in Reducer Actions

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)

Redux not Re-rendering React components even though store is updated

Hi I'm new to Redux and I'm using React and Redux to try to build a UI where I can drag and drop files (invoices in this case) into a portion of the UI, render them in a list and then be able to launch a popover to edit the metadata associated with each invoice. Dragging and dropping is all working fine - Redux is re-rendering the view each time a file is dropped and the list is being updated. However, when I try an click the edit button against each invoice the store is being updated but the props in my popover component are not. Indeed, it doesn't look like any re-rendering is happening at all when I attempt to click the edit invoice button
App.js
import React from 'react'
import AddInvoice from '../containers/AddInvoice'
import CurrentInvoiceList from '../containers/CurrentInvoiceList'
import ControlPopover from '../containers/ControlPopover'
const App = () => (
<div>
<AddInvoice />
<CurrentInvoiceList />
<ControlPopover />
</div>
)
export default App
containers/AddInvoice.js
import React from 'react'
import { connect } from 'react-redux'
import { addInvoice } from '../actions'
const recipientDataDefaults = {
name: '',
surname: '',
address: '',
phone: ''
};
const handleDragOver = event => {
event.stopPropagation();
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
const handleDragEnter = event => {
event.stopPropagation();
event.preventDefault();
};
const handleDragLeave = event => {
event.stopPropagation();
event.preventDefault();
};
let AddInvoice = ({ dispatch }) =>
const styles = {'minHeight': '200px', 'background': 'tomato'}
return (
<div style={styles}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={event => {
event.stopPropagation();
event.preventDefault();
const data = event.dataTransfer;
const files = data.files;
const newInvoiceUploads = Object.keys(files)
.map(key => files[key])
.map(file => {
const invoiceObject = {};
invoiceObject.files = [file];
invoiceObject.recipientData = Object.assign({}, recipientDataDefaults);
return invoiceObject;
});
newInvoiceUploads.forEach(invoice => dispatch(addInvoice(invoice)))
}}>
Drag an invoice here to upload
</div>
)
}
AddInvoice = connect()(AddInvoice)
export default AddInvoice
containers/ControlPopover.js
import { connect } from 'react-redux'
import { closePopoverWithoutSave } from '../actions'
import Popover from '../components/Popover/Popover'
const mapStateToProps = (state) => {
return {
isActive: !!state.isActive
}
}
const mapDispatchToProps = {
handleCancel: closePopoverWithoutSave
}
const ControlPopover = connect(
mapStateToProps,
mapDispatchToProps
)(Popover)
export default ControlPopover
containers/CurrentInvoiceList.js
import { connect } from 'react-redux'
import { showInvoiceEditPopover } from '../actions'
import InvoiceList from '../components/InvoiceList/InvoiceList'
const mapStateToProps = state => {
return {
invoices: state.invoices
}
}
const mapDispatchToProps = dispatch => ({
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
})
const CurrentInvoiceList = connect(
mapStateToProps,
mapDispatchToProps
)(InvoiceList)
export default CurrentInvoiceList
actions/index.js
let nextInvoiceId = 0
export const addInvoice = invoice => ({
type: 'ADD_INVOICE',
id: nextInvoiceId++,
invoiceData: invoice
})
export const showInvoiceEditPopover = invoice => ({
type: 'SHOW_POPOVER',
invoice
})
The popover reducer (combined in app but inlined here for brevity) reducers/index.js
const popover = (state = {}, action) => {
switch (action.type) {
case 'SHOW_POPOVER':
const popoverState = {}
popoverState.isActive = true
popoverState.data = action.invoice
return popoverState
case 'CLOSE_POPOVER_WITHOUT_SAVING':
const inactiveState = {}
inactiveState.isActive = false
inactiveState.data = {}
return inactiveState;
default:
return state
}
}
export default popover
components/InvoiceList.js
import React from 'react'
import PropTypes from 'prop-types'
import Invoice from '../Invoice/Invoice'
const InvoiceList = ({ invoices, handleEditInvoice }) => {
return (
<div>
{invoices.map(invoice =>
<Invoice
key={invoice.id}
invoice={invoice.invoiceData}
onClick={event => {
// here we invoke the action bound by the CurrentInvoiceList
// container
event.preventDefault()
handleEditInvoice(invoice)
}}
/>
)}
</div>
)
}
InvoiceList.propTypes = {
invoices: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
invoiceData: PropTypes.object
}).isRequired).isRequired,
handleEditInvoice: PropTypes.func.isRequired
}
export default InvoiceList
components/Invoice.js
import React from 'react'
import PropTypes from 'prop-types'
import TextInput from '../TextInput/TextInput';
import Button from '../Button/Button';
import './invoice.css'
const Invoice = ({ invoice, onClick }) => {
const fileNames = invoice.files.map((file, index) => {
return (<div key={index} className="invoice__file-title-legend">
{file.name}</div>);
});
return (
<div className="invoice">
<form className="invoice__form">
<TextInput id="invoice__input-amount" placeholder="enter invoice amount" label="Invoice Amount" />
<TextInput id="invoice__input-target" placeholder="enter payment target" label="Payment Target" />
<Button value="Add recipient" onClick={onClick} /> // clicking this button updates the store but does NOT re-render. Why?
</form>
<div className="invoice__files">{fileNames}</div>
</div>
)
}
Invoice.propTypes = {
onClick: PropTypes.func.isRequired,
invoice: PropTypes.object
}
export default Invoice
components/Popover/Popover.js
import React from 'react'
import PropTypes from 'prop-types'
import createModifiers from '../../lib/createModifiers';
import './Popover.css';
const Popover = ({ handleCancel, isActive }) => {
console.log('rendering popover component') // does not get called when invoice edit button is pressed
const popoverModifiers = createModifiers('popover', {
'is-active': isActive
})
return (
<div className={popoverModifiers}>
<div className="popover__header">
<button onClick={handleCancel}>x</button>
</div>
<div className="popover__content">
Popover content
</div>
</div>
)
}
Popover.propTypes = {
handleCancel: PropTypes.func.isRequired,
isActive: PropTypes.bool.isRequired
}
export default Popover
And finally the createModifiers Code for posterity. This code is merely producing some BEM modifier CSS classes based on a state boolean value passed in
const createModifiers = (element, modifiers) => {
const classList = Object.keys(modifiers)
.filter(key => modifiers[key])
.map(modifier => `${element}--${modifier}`)
.concat(element)
.join(' ')
return classList
}
export default createModifiers
I know this is a large amount of example code so I tried to keep it a brief and focused as possible whilst giving a comprehensive view of the application. Any help is most appreciated.
The problem is in containers/ControlPopover.js and the mapStateToProps function. The isActive property needs to be assigned to state.popover.isActive
I believe your problem is your mapDispatchToProp functions are not formatted properly.
You need to return an object that has methods. Those methods are what will be given to your connected component as props.
Example:
const mapDispatchToProps = ( dispatch ) => {
return {
doSomething: ( arguments ) => {
// here you can dispatch and use your arguments
}
};
}
doSomething is the prop that would be provided to the connected component.
All of your mapDispatchToProps functions are formatted improperly.
SIDE NOTE / OPINION - TLDR:
In the future if you have a lot of code to post, I believe it would be easier to digest if the pieces were linked together.
I.E.
// App.js
const App = () => (
<div>
<Header />
<Body />
<Footer />
</div>
);
The components appear in the order: header -> body -> footer. Provide the code for them in that order, with their actions, reducer, presentational, and container information in one block.
Header
// header.presentational.js ...
// header.container.js ... ( or where you mapStateToProps and connect )
// header.actions.js ...
// header.reducer.js ...
Body ...
Footer ...
I don't know if the code is different on your end, but your mapStateToDispatch function is still improperly formatted.
Change this...
const mapDispatchToProps = dispatch => ({
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
})
To this:
const mapDispatchToProps = dispatch => ({
return {
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
};
})

React/Redux—Individual state for each Instance of a Component

If have a list of users and each Entry has a button »EDIT«. If the user clicks on it the following happens:
request the server for the form
Add the component <UserEditForm /> to the entry, what expands the entry
This works fine except one thing: If one clicks further buttons each Instance of the form receives the data of the last user form requested. That is because I have only only one userform property in the state.
So to solve this I want to exchange userform to userforms which should/could be an Object like that:
userforms: {
<id of first instance>: { … }, //formdata
<id of second instance>: { … },
…
}
But since I am new to React/Redux I do not really know how to do that, or what the »right« approach, or best practice is, to actually do it.
My Idea is to create a higher Order Component like so:
import React from 'react';
import {connect} from 'react-redux';
import {uuid} from '../../helpers/uuid';
export const formdatainstance = (FormInstance) => {
let id = null;
class FormDataMapper extends React.Component {
constructor (props) {
super(props);
id = uuid();
}
render () {
//extract the formdata from the state
//using the id
return <FormInstance { ...this.props } />
}
}
const mapStateToProps = (state) => {
console.log(id); //is null for one run
return {
userforms: state.userforms
};
};
return connect(mapStateToProps)(FormDataMapper);
}
So in the List component I can:
import UserEditForm from './UserEditForm';
import {formdatainstance} from './formdatainstance';
const MappedUserEditForm = formdatainstance(UserEditForm);
class List extends React.Component {
render(){
return (
{users.map(user => {
//more stuff
<MappedUserEditForm />
//more stuff
})}
);
}
}
So my Question: Is this a good Idea? If yes what would be the proper way to do the cleanup, so when in the life cycle of the component should I delete the data from the state? Is there another way to do that, which is easier?
Thanks for Help!
Here's what you can do...
import React from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { reduxForm } from 'redux-form';
class UserEditForm extends Component {
...
render() {
return <form onSubmit={this.props.handleSubmit(this.props.onSubmit)}>
...form fields
</form>
}
}
const mapStateToProps = (state, ownProps) => {
return {
form: ownProps.formId
}
}
export default compose(
connect(mapStateToProps),
reduxForm({
//...other redux-form options
})
)(UserEditForm);
Your ListComponent
render() {
return <ul>
{this.props.users.map(user =>
<li key={user.id}>
...
<UserEditForm formId={'form-' + user.id} onSubmit={...} />
</li>
)}
</ul>
}
This allows you to have a dynamic form name.
Even if the answer of #jpdelatorre seems to be the best hit for me, since it also includes the link to redux-forms, what will probably help me a lot, I would like to post my working solution here, just in case somebody might find it useful. It just hit me over night, so needed to test if my thought were right, what I could finally proof.
I was not able to do the whole Mapping with a sole HOC and I needed to add/modify reducers too. Basically it works that way:
Data Mapping is done by ID,
the original action creators are wrapped, such that the used id is attached to the Object
the reducers are wrapped two and called by the »datamapped« reducer
So the code of the original reducers and action creators does not need to be changed, what makes the wrapping kind of easy to use. I first wanted to use uuid's which are created on the fly, but I discarded that, to make possible to save and restore the whole application state.
so the HOC code is that:
import React from 'react';
import {connect} from 'react-redux';
// The Component to wrap,
// all of its actions
// its default state
export const formdatainstance = (FormInstance, Actions, defaultState = {}) => {
const mapStateToProps = (state) => {
return {
mappedData: state.mappedData
};
};
class FormDataMapper extends React.Component {
static propTypes = {
id: React.PropTypes.string.isRequired
};
static contextTypes = {
store: React.PropTypes.object
};
//most of mapping happens here
render () {
//wrap the action creators
const actions = Object.keys(Actions).reduce((list, key) =>{
list[key] = (...args) => {
const action = Actions[key](...args);
//handle asyn operations as well
if('then' in action && typeof action['then'] == 'function') {
action.then(data => {
//attaching the id
this.props.dispatch({...data, id: this.props.id});
});
} else {
//attach the id
this.context.store.dispatch({...action, id: this.props.id });
}
};
return list;
}, {}),
//there wont be any data at first, so the default state is handed
//over
mappedProps = this.props.mappedData.hasOwnProperty(this.props.id) ?
this.props.mappedData[this.props.id] : defaultState;
//merge the hotchpotch
let props = Object.assign({}, mappedProps, this.props, actions);
//clean up
delete props.id;
delete props.mappedData;
return <FormInstance { ...props } />
}
}
return connect(mapStateToProps)(FormDataMapper);
};
the reducer code:
//hlper method
export const createTypesToReducerMap = (types, reducer) => {
return Object.keys(types).reduce((map, key) => {
map[types[key]] = reducer;
return map;
}, {});
}
export const createMappedReducer = (reducerMap, defaultState = {}) => {
const HANDLERS = reducerMap.reduce((handlers, typeMap) => {
return { ...handlers, ...typeMap };
},{});
return (state, action) => {
if (!action.hasOwnProperty('id')) {
if (state === undefined) return defaultState;
return state;
}
const reducer = HANDLERS.hasOwnProperty(action.type) ?
HANDLERS[action.type] : null;
let a = {...action};
delete a.id;
return reducer !== null ?
Object.assign({}, state, { [action.id]: reducer(state[action.id], a)}) :
state;
}
}
and finally the store:
const userEditTypeReducerMap = createTypesToReducerMap(userEditTypes, userFormReducer);
const reducer = combineReducers({
…
mappedData: createMappedReducer(
[userEditTypeReducerMap], {})
…
});
export default compose(
applyMiddleware(
thunk
)
)(createStore)(reducer, {});

Implementing React Redux

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.

Resources