Store ui related properties outside of the component state - reactjs

I've moved from using pure jsons in react to instances.
Meaning, I store instances inside the component state.
The problem is that the instances can't update their own properties because it means mutating the state so I need to clone the instance, update from outside of the instance, and then set state.
I wanted to store the instances on the component and store a dummy state property that once an instance property changes it updates the dummy state that will trigger a re-render.
Since the instances are related to the UI I know they should reside in the state.
I know it's not the best practice but I'm trying to figure out how bad is it.
For example:
class Team {
constructor(data) {
this._teamMates = null;
this.id = data.id;
}
get teamMates() {
if (!this._teamMates) {
this.fetchTeamMates();
return null;
}
else
return this._teamMates;
}
fetchTeamMates() {
fetch('/teamMates/' + this.id).then(teamMates => this._teamMates = teamMates);
}
}
When we first try to get teamMated they are not valid so we fetch them and on the next state change I want them to be valid as teamMates.
I can return a promise instead of null, it is possible but I want to handle it this way because I want to have conditional rendering if we have the teamMates.
I know there are multiple ways to solve this thing but I rather try to find a way to make it work without returning a promise.

I'm not exactly sure what you mean by:
"The problem is that the instances can't update their own properties because it means mutating the state so I need to clone the instance, update from outside of the instance, and then set state."
It sounds like the simplest thing to do is create the state and method in a parent HOC, pass them to a child component and fire the action in an event handler, it should then update the parents state and cause the normal render in the child. Ideally your child would be a "dumb component", literally just passing the values and actions and rendering.

I think you are looking for some state manager, like redux for example (you didn't exclude it from your question)
If you really want to keep your own instances (what I don't see as a necessity, as it is in no way necessary, you are handling with state), then you should create an immutable version of your classes, meaning, if you update a player inside your team, you should return a new version of your team instance, with the changed player data.
The below example doesn't use an instance, but it moves the team mates to a store, and lets redux manage the state (so it removes it from component state).
Main key points to focus on would be:
The connect statement, connecting a component with a (part of) state injected in its props, and adding a dispatcher that handles the actions you dispatch to it, looking like this:
const ConnectedTeamEditor = connect( teamStateToProps, playerDispatcher )( TeamEditor );
This requires then ofcourse a state mapper and the dispatcher, which looks pretty much like this:
// dispatcher for the actions
const playerDispatcher = dispatch => ({
fetch() {
return getTeamMates().then( response => dispatch( { type: 'loaded', payload: response } ) );
},
update( player ) {
dispatch( { type: 'update', payload: player } );
}
});
// state mapper, sharing teamMates state over the connected components props
const teamStateToProps = state => ({ teamMates: state.teamMates });
To handle the dispatch calls, we would need a reducer that is then also registered into a store, and we use the createStore method provided by react-redux. The reducer can have a default state
// reducer for player actions
const playerReducer = ( state = { teamMates: null }, action ) => {
switch ( action.type ) {
case 'loaded':
return { teamMates: action.payload };
case 'update':
return { teamMates: state.teamMates.map( player => player.id === action.payload.id ? action.payload : player ) };
default:
return state;
}
};
// and registration to store
const appStore = createStore( playerReducer );
React-redux will automatically update the affected components, once their state gets updated. Important to note is that state shouldn't be mutated, it should be replaced when required. It is also important that the state is returned when no matching action.type was found.
Finally, the application should be wrapped with a Provider that receives the store through its props, and that would then look like that.
ReactDOM.render( <Provider store={appStore}><ConnectedTeamEditor /></Provider>, target );
I do keep a component state, but only for updating the selected player at the time.
As most of us are in WK mood, I kinda went freestyle on the design, just run the code to see what I mean ^_^
const { createStore } = Redux;
const { Provider, connect } = ReactRedux;
// data provider
function getTeamMates() {
return Promise.resolve([
{ id: 1, firstName: 'Romelu', lastName: 'Lukaku', position: 'forward' },
{ id: 2, firstName: 'Dries', lastName: 'Mertens', position: 'forward' },
{ id: 3, firstName: 'Eden', lastName: 'Hazard', position: 'forward' },
{ id: 4, firstName: 'Radja', lastName: 'Naingolan', position: 'midfield' },
{ id: 5, firstName: 'Kevin', lastName: 'De Bruyne', position: 'midfield' },
{ id: 6, firstName: 'Jordan', lastName: 'Lukaku', position: 'defender' },
{ id: 7, firstName: 'Axel', lastName: 'Witsel', position: 'midfield' },
{ id: 8, firstName: 'Vincent', lastName: 'Kompany', position: 'defender' },
{ id: 9, firstName: 'Thomas', lastName: 'Meunier', position: 'defender' },
{ id: 10, firstName: 'Marouane', lastName: 'Fellaini', position: 'midfield' },
{ id: 11, firstName: 'Thibaut', lastName: 'Courtois', position: 'Goalie' }
]);
}
// some container to translate the properties to readable names
const labels = {
'id': '#',
'firstName': 'First Name',
'lastName': 'Last Name',
'position': 'Position'
};
// small function to return the correct translation or default value
const translateProperty = ( property ) => labels[property] || property;
// a field that is either editable or just shows a span with a value
const Field = ({ isEditable, value, onChange }) => {
if ( isEditable ) {
return <span className="value">
<input type="text" value={ value } onChange={ e => onChange( e.target.value ) } />
</span>;
}
return <span className="value">{ value }</span>;
};
// a single player, delegating changes to its data and selection style
const Player = ({ player, isSelected, onChange, onSelect }) => {
return (
<div className={classNames('row', { isSelected })} onClick={ () => !isSelected && onSelect && onSelect( player ) }>
{ Object.keys( player ).map( property => (
<div className="cell" key={property}>
<span className="label">{ translateProperty( property ) }</span>
<Field
isEditable={isSelected && property !== 'id' }
value={ player[property] }
onChange={ newValue => onChange( {...player, [property]: newValue } ) }
/>
</div>
) ) }
</div>
);
};
// reducer for player actions
const playerReducer = ( state = { teamMates: null }, action ) => {
switch ( action.type ) {
case 'loaded':
return { teamMates: action.payload };
case 'update':
return { teamMates: state.teamMates.map( player => player.id === action.payload.id ? action.payload : player ) };
case 'add':
return { teamMates: state.teamMates.concat( [ {
id: state.teamMates.reduce( (c, i) => c > i.id ? c : i.id, 0 ) + 1,
...action.payload } ] ) };
default:
return state;
}
};
// dispatcher for the actions
const playerDispatcher = dispatch => ({
fetch() {
return getTeamMates().then( response => dispatch( { type: 'loaded', payload: response } ) );
},
update( player ) {
dispatch( { type: 'update', payload: player } );
},
add( player ) {
dispatch( { type: 'add', payload: player } );
}
});
const teamStateToProps = state => ({ teamMates: state.teamMates });
// the team editor that works with props
class TeamEditor extends React.Component {
constructor() {
super();
this.state = {
selectedPlayer: null,
newPlayer: null
};
}
componentWillMount() {
// load when mounting
this.props.fetch();
}
selectPlayer( player ) {
// component state keeps the selected player
this.setState( { selectedPlayer: player } );
}
updatePlayer( player ) {
// update player through dispatcher
this.props.update( player );
}
onAddClicked() {
this.setState( {
isAdding: true,
newPlayer: {
firstName: '',
lastName: '',
position: ''
}
} );
}
updateNewPlayer( player ) {
this.setState( { newPlayer: player } );
}
savePlayer() {
this.setState( { isAdding: false } , () => this.props.add( this.state.newPlayer ) );
}
cancelChanges() {
this.setState( { isAdding: false } );
}
render() {
const { teamMates} = this.props;
const { selectedPlayer, isAdding, newPlayer } = this.state;
return (
<div className="team">
<div className="row">
{ !isAdding && <button
type="button"
onClick={ () => this.onAddClicked() }>Add team member</button>
}
{ isAdding && <span>
<button
type="button"
onClick={ () => this.savePlayer() }>Save</button>
<button
type="button"
onClick={ () => this.cancelChanges() }>Cancel</button>
</span> }
</div>
{ isAdding && <div className="row">
<Player
isSelected
player={newPlayer}
onChange={ (...args) => this.updateNewPlayer( ...args ) } />
</div> }
{ teamMates && teamMates.map( player => (
<Player
key={player.id}
isSelected={ !isAdding && selectedPlayer && selectedPlayer.id === player.id }
player={player}
onChange={ (...args) => this.updatePlayer( ...args ) }
onSelect={ (...args) => this.selectPlayer( ...args ) }
/> ) ) }
</div>
);
}
}
// connect the teameditor with state and dispatcher
const ConnectedTeamEditor = connect( teamStateToProps, playerDispatcher )( TeamEditor );
// create a simple store, no middleware
const appStore = createStore( playerReducer );
const target = document.querySelector('#container');
ReactDOM.render( <Provider store={appStore}><ConnectedTeamEditor /></Provider>, target );
* { box-sizing: border-box; }
body { margin: 0; padding: 0; }
.row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-evenly;
align-content: center;
}
.row:hover {
cursor: pointer;
}
.row.isSelected > .cell > * {
background-image: linear-gradient( rgba( 225, 225, 225, 0.7 ), rgba( 255, 255, 255, 0.9 ) );
}
.cell {
display: flex;
flex-direction: column;
flex-basis: 25%;
flex-grow: 0;
flex-shrink: 0;
align-self: flex-start;
background-color: yellow;
}
.cell:first-child {
background-color: red;
}
.cell:last-child {
background-color: black;
color: #fff;
}
.cell > span {
padding: 5px;
}
.cell > span > input {
width: 100%;
}
.label {
text-transform: capitalize;
}
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<script id="classnames" src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/index.js"></script>
<script id="redux" src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.7.2/redux.js"></script>
<script id="react-redux" src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.6/react-redux.js"></script>
<div id="container"></div>

Related

How to change property of object in React, Recoil

I'm working on a shopping cart.
<Cart /> is Cart Page which render Products in cartList Array.
<CartProduct /> render each one product in cartList Array
I want to make the quantity data change, when i click quantity button.
Here is My First Try Code
function Cart(props) {
const cartsList = useRecoilValue(cartsListState);
return(
{cartsList
.filter(cart => cart.keep === 'cold')
.map((cart) => {
return <CartProduct cart={cart} getNowQuantity={getNowQuantity} />
})
}
{cartsList
.filter(cart => cart.keep === 'freeze')
.map((cart) => {
return <CartProduct cart={cart} getNowQuantity={getNowQuantity} />
})
}
{cartsList
.filter(cart => cart.keep === 'normal')
.map((cart) => {
return <CartProduct cart={cart} getNowQuantity={getNowQuantity} />
})
}
)
}
function CartProduct({ cart, getNowQuantity}) {
const [cartsList, setCartsList] = useRecoilState(cartsListState);
return(
const PlusQuantity = () => {
setCounterQuantity(counterQuantity => counterQuantity + 1);
cart.quantity += 1;
getNowQuantity(cart.quantity);
}
const MinusQuantity = () => {
if (cart.quantity>=2) {
setCounterQuantity(counterQuantity => counterQuantity - 1);
cart.quantity -= 1;
getNowQuantity(cart.quantity);
}
else return;
}
)
}
Firts code make error
Uncaught TypeError: Cannot assign to read only property 'quantity' of
object '#
So i tried to use spread operator in CartProduct.js Like this way
const cartProduct = ([ ...cart]);
return(
CartProduct.quantity = +1
~~
~~~
)
This code make error
cart is not iterable
so i tried
let iterableCart = cart[Symbol.iterator]
It doesn't work.
How can i change cart.property for ChangeQuantityButton?
By default Recoil makes everything immutable so code like this
cart.quantity += 1;
won't work because you're trying to update a value on a frozen object.
Instead you need to create a new object, and use the existing values to update it.
Here I'm using a sample data set and using it to build three product items using an <Item> component. This component displays the name, the current quantity, and two buttons to decrement/increment the values. On the buttons are a couple of data attributes that identify the product id, and the button action.
When a button is clicked the handleClick function is called. This destructures the id and action from the button dataset, and then map over the current cart using those values to check the cart items and return updated objects to the state.
const { atom, useRecoilState, RecoilRoot } = Recoil;
// Initialise the cart data
const cart = [
{ id: 1, name: 'Banana', qty: 0 },
{ id: 2, name: 'Beef', qty: 0 },
{ id: 3, name: 'Mop', qty: 0 }
];
// Initialise the cart atom setting its
// default to the cart data
const cartAtom = atom({
key: 'cartAtom',
default: cart
});
function Example() {
// Use the recoil state
const [ cart, setCart ] = useRecoilState(cartAtom);
// When a button is clicked
function handleClick(e) {
// Get its id and action
const { dataset: { id, action } } = e.target;
// Update the cart using the id to identify the item
// and return an updated object where appropriate
setCart(prev => {
return prev.map(item => {
if (item.id === +id) {
if (action === 'decrement' && item.qty > 0) {
return { ...item, qty: item.qty - 1 };
}
if (action === 'increment') {
return { ...item, qty: item.qty + 1 };
}
}
return item;
});
});
}
// Iterate over the cart data using the Item
// component to display the item details
return (
<div>
{cart.map(item => {
const { id, name, qty } = item;
return (
<Item
key={id}
id={id}
name={name}
qty={qty}
handleClick={handleClick}
/>
);
})}
</div>
);
}
function Item({ id, name, qty, handleClick }) {
return (
<div className="item">
<span className="name">{name}</span>
<button
data-id={id}
data-action="decrement"
type="button"
onClick={handleClick}
>-
</button>
{qty}
<button
data-id={id}
data-action="increment"
type="button"
onClick={handleClick}
>+
</button>
</div>
);
}
ReactDOM.render(
<RecoilRoot>
<Example />
</RecoilRoot>,
document.getElementById('react')
);
.item:not(:last-child) { margin-bottom: 0.25em; }
.name { display: inline-block; width: 60px; }
button { width: 30px; height: 30px; margin: 0 0.25em; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/recoil#0.7.6/umd/index.js"></script>
<div id="react"></div>

React Native Dynamic View Mapped from Array Does Not Update When Element Changes

I have a view in which I am dynamically adding Input components from React Native Elements. Ultimately, I need to validate the text typed in Input, and so, for now, I am trying to change the errorMessage prop from false to true upon onChangeText. Unfortunately, nothing I try changes the errorMessage even though the state component changes to true. I have a Snack, and, for convenience, my code follows:
import * as React from 'react'
import { ScrollView, StyleSheet, View } from 'react-native'
import { Button, Icon, Input, Text } from 'react-native-elements'
import * as Shortid from 'shortid'
export default class App extends React.Component {
constructor (props) {
super(props)
this.state = {
timeInputs: [],
validities: []
}
this.addTimeInput = this.addTimeInput.bind(this)
this.removeTimeInput = this.removeTimeInput.bind(this)
this.mutateValidities = this.mutateValidities.bind(this)
}
componentDidMount () {
this.addTimeInput()
}
addTimeInput () {
const identifier = Shortid.generate()
const timeInputs = this.state.timeInputs
const validities = this.state.validities
let isTimeValid = false
let time = ''
new Promise(
(resolve, reject) => {
resolve(
validities.push(
{ id: identifier, value: isTimeValid }
),
timeInputs.push(
<View key = { identifier }>
<View style = { styles.row }>
<Input
errorMessage = {
this.state.validities.filter(
validity => validity.id !== identifier
).value == true
? 'Valid'
: 'Invalid'
}
errorStyle = { styles.error }
onChangeText = {
value => {
time = value
this.mutateValidities(identifier, true)
console.log('TIME ' + identifier + ': ' + time)
}
}
placeholder = 'HH:MM AM/PM'
ref = { React.createRef() }
/>
<Icon
color = { colors.dark }
name = 'add-circle'
onPress = { () => this.addTimeInput() }
type = 'material'
/>
<Icon
color = { colors.dark }
name = 'remove-circle'
onPress = {
() => {
if (this.state.timeInputs.length > 1) {
this.removeTimeInput(identifier)
} else {
console.log('LENGTH: ' + this.state.timeInputs.length)
}
}
}
type = 'material'
/>
</View>
</View>
)
)
}
)
.then(
this.setState(
{
timeInputs: timeInputs,
validities: validities
}
)
)
.catch(
(reason) => {
console.log(
'Failed to create time-input because of the following: ' + reason
)
}
)
}
mutateValidities (key, value) {
this.setState(
{
validities: this.state.validities.map(
validity => validity.id === key
? {...validity, value: value}
: validity
)
}
)
}
removeTimeInput (key) {
this.setState(
{
timeInputs: this.state.timeInputs.filter(
timeInput => timeInput.key !== key
),
validities: this.state.validities.filter(
validity => validity.id !== key
)
}
)
}
render () {
return (
<ScrollView contentContainerStyle = { styles.container }>
<Text h4 style = { styles.title }>Time Inputs</Text>
{
this.state.timeInputs.map(
(value) => { return value }
)
}
<Button
buttonStyle = { styles.button }
onPress = {
() => this.state.validities.map(
validity => console.log(validity.id + validity.value)
)
}
title = 'Log Validities'
/>
</ScrollView>
)
}
}
const colors = {
dark: 'steelblue',
light: 'aliceblue',
medium: 'lightsteelblue',
error: 'firebrick'
}
const styles = StyleSheet.create(
{
button: {
backgroundColor: colors.dark,
margin: 5
},
container: {
alignItems: 'center',
backgroundColor: colors.light,
flex: 1,
justifyContent: 'center'
},
error: {
color: colors.error,
fontSize: 12,
margin: 5
},
row: {
alignItems: 'center',
flexDirection: 'row',
margin: 5,
width: '80%'
},
title: {
margin: 5
}
}
)
Due credit goes to McGregor (2017) for getting me this far, but I am still stuck.
Reference:
McGregor, L. (2017, October 2) Whats the best way to update an object in an array in ReactJS? [Stack Overflow answer]. Retrieved from https://stackoverflow.com/a/46518653/6084947
I believe my problem was that I was trying to trigger changes in the view that were too deeply nested. Even assuming the props were passed correctly, the view would not have updated when the state changed because React only compares props and state so far. Following the recommendation made in "The Power Of Not Mutating Data" (Facebook, 2019), I avoided these issues with shallow comparison by making the text-input a full React component. I also applied React Redux to manage the state in a central store instead of passing props back and forth, which I never could get to work even after abstracting the text-input.
The code is now spread out between too many different files to post here, but anyone interested can view it on GitHub or Snack.
Reference:
Facebook. (2019). Optimizing Performance [Documentation]. Retrieved from https://reactjs.org/docs/optimizing-performance.html#the-power-of-not-mutating-data

React Select with Custom MultiValueContainer and Apollo Mutation not Cooperating

I have a custom component that uses Apollo and React-Select and has two mutations (see below). The react-select is multivalue and needs to be custom because I need an "isSelected" checkbox on it. Not shown in this code, but the initial options list is passed in from the parent container.
The parent Select and its mutation work as expected. However, I'm running into a couple of odd problems with the custom MultiValueContainer. The first is that the first time I select any of the check-boxes, I get an error saying that "Can't call setState (or forceUpdate) on an unmounted component." Note below that I have an empty componentWillUnmount function just to see if it gets called, which it doesn't. But (I assume) as a result of that, when the "toggleThing" mutation is called, the state doesn't have the vars necessary to complete the request. The second time I click it works as expected, with the exception of the second issue.
The second issue is that the onCompleted function on the MultiValueContainer mutation never fires, so even though the server is returning the expected data, it never seems to get back to the mutation and therefore never to the component. The onCompleted function on the parent Select works as expected.
Thanks in advance for any insights anyone might have. Perhaps needless to say, I am relatively new to react/apollo/react-select and apologize in advance for any newbie mistakes. Also, I've tried to scrub and simplify the code so apologies also for any renaming mistakes.
const UPDATE_THINGS = gql`
mutation UpdateThings(
$id: ID!
$newThings: [ThingInput]
) {
updateThings(
id: $id
newThings: $newThings
) {
id
}
}
`;
const TOGGLE_THING = gql`
mutation ToggleThing($id: ID!, $isChecked: Boolean) {
toggleThing(
id: $id
isChecked: $isChecked
) {
id
}
}
`;
class ThingList extends Component {
stylesObj = {
multiValue: base => {
return {
...base,
display: 'flex',
alignItems: 'center',
paddingLeft: '10px',
background: 'none',
border: 'none'
};
}
};
constructor(props) {
super(props);
this.state = {
selectedThings: [],
selectedThingId: '',
selectedThingIsChecked: false
};
}
onUpdateComplete = ({ updateThings }) => {
console.log('onUpdateComplete');
console.log('...data', updateThings );
this.setState({ selectedThings: updateThings });
};
onToggleThing = (thingId, isChecked, toggleThing) => {
console.log('onToggleThing, thingId, isChecked');
this.setState(
{
selectedThingId: thingId,
selectedThingIsChecked: isHighPisCheckedoficiency
},
() => toggleThing()
);
};
onToggleThingComplete = ({ onToggleThing }) => {
console.log('onToggleThingComplete ');
console.log('...data', onToggleThing );
this.setState({ selectedThings: onToggleThing });
};
handleChange = (newValue, actionMeta, updateThings) => {
this.setState(
{
selectedThings: newValue
},
() => updateThings()
);
};
isThingSelected = thing=> {
return thing.isSelected;
};
getSelectedThings = selectedThings => {
console.log('getSelectedSkills');
return selectedThings ? selectedThings.filter(obj => obj.isSelected) : [];
};
componentWillUnmount() {
console.log('componentWillUnmount');
}
render() {
const self = this;
const MultiValueContainer = props => {
// console.log('...props', props.data);
return (
<Mutation
mutation={ TOGGLE_THING }
onCompleted={self.onToggleThingComplete}
variables={{
id: self.state.selectedThingId,
isChecked: self.state.selectedThingIsChecked
}}>
{(toggleThing, { data, loading, error }) => {
if (loading) {
return 'Loading...';
}
if (error) {
return `Error!: ${error}`;
}
return (
<div className={'option d-flex align-items-center'}>
<input
type={'checkbox'}
checked={props.data.isChecked}
onChange={evt => {
self.onToggleThing(
props.data.id,
evt.target.checked,
toggleIsHighProficiency
);
}}
/>
<components.MultiValueContainer {...props} />
</div>
);
}}
</Mutation>
);
};
return (
<Mutation
mutation={UPDATE_THINGS}
onCompleted={this.onUpdateComplete}
variables={{ id: this.id, newThings: this.state.selectedThings}}>
{(updateThings, { data, loading, error }) => {
if (loading) {
return 'Loading...';
}
if (error) {
return `Error!: ${error}`;
}
return (
<div>
<Select
options={this.props.selectedThings}
styles={this.stylesObj}
isClearable
isDisabled={this.props.loading}
isLoading={this.props.loading}
defaultValue={this.props.selectedThings.filter(
obj => obj.isSelected
)}
isOptionSelected={this.isOptionSelected}
isMulti={true}
onChange={(newValue, actionMeta) =>
this.handleChange(
newValue,
actionMeta,
updateThings
)
}
components={{
MultiValueContainer
}}
/>
</div>
);
}}
</Mutation>
);
}
}
export default ThingsList;
You are redefining MultiValueContainer on every render, which is not a good practice and may cause unexpected behavior. Try moving it into separate component to see if it helps.

Redux state shape for managing multiple React component instances?

Working on a small project using React and Redux in which I'm making what is essentially a Trello clone, or kanban system. But I'm having trouble figuring out how to construct my state for Redux so that things don't get weird.
Essentially, a user needs to be able to create multiple instances of a <Board/> component via React. Each <Board/> instance has a title. Continuing to drill down, each </Board> instance can also have an array of its own <List/> component instances. Each <List/> instance can in turn have its own <Card/> component instances.
Which is where I get confused. In React, it's simple--each instance of <Board/> and each instance of <List/> just manage their own state. But I can't figure out how to refactor everything so that Redux manages all state, but each component instance receives the correct slice of state.
So far, I've constructed my Redux state to look as follows. Any help is appreciated!
{
boards: [
{
title: 'Home',
lists: [
{
title: 'To Do',
cards: [
{ title: 'Finish this project.'},
{ title: 'Start that project.'}
]
},
{
title: 'Doing',
cards: []
},
{
title: 'Done',
cards: []
}
]
}
]
}
redux is basically a one global store object. so theoretically this is no different than using react without redux but keeping the store at the most top level component's state.
Of course with redux we gain a lot of other goodies that makes it a great state manager. But for the sake of simplicity lets focus on the state structure and data flow between the react components.
Lets agree that if we have one global store to hold our single source of truth then we don't need to keep any local state inside our child components.
But we do need to break and assemble our data within our react flow, so a nice pattern is to create little bits of components that just get the relevant data an id and handlers so they can send back data to the parents with the corresponding id. This way the parent can tell which instance was the one invoking the handler.
So we can have a <Board /> that renders a <List /> that renders some <Cards /> and each instance will have it's own id and will get the data it needs.
Lets say we want to support addCard and toggleCard actions, We will need to update our store in couple level for this.
For toggling a card we will need to know:
What is the Card id that we just clicked on
What is the List id that this card belongs to
What is the Board id that this list is belong to
For adding a card we will need to know:
What is the List id that we clicked on
What is the Board id that this list is belong to
Seems like the same pattern but with different levels.
To do that we will need to pass onClick events to each component and this component will invoke it while passing it's own id to the parent, in turn the parrent will invoke it's onClick event while passing the child's id and it's own id so the next parent will know which child instances were being clicked.
For example:
Card will invoke:
this.props.onClick(this.props.id)
List will listen and will invoke:
onCardClick = cardId => this.props.onClick(this.props.id,cardId);
Board wil llisten and will invoke:
onListClick = (listId, cardId) => this.props.onClick(this.props.id, listId, cardId)
Now our App can listen as well and by this time it will have all the necessary data it needs to perform the update:
onCardToggle(boardId, listId, cardId) => dispatchToggleCard({boardId, listId, cardId})
From here its up to the reducers to do their job.
See how the components transfer data upwards, each component gather the data sent from its child and passing it upwards while adding another piece of data of itself. Small bits of data are assembled up until the top most component get all the data it needs to perform the updates to the state.
I've made a small example with your scenario, note that i'm not using redux due to the limitations of stack-snippets. I did wrote however the reducers and the entire logic of the updates and data flow, but i skipped the action creators part and connecting to an actual redux store.
I think it can give you some idea on how to structure your store, reducers and components.
function uuidv4() {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}
class Card extends React.Component {
onClick = () => {
const { onClick, id } = this.props;
onClick(id);
}
render() {
const { title, active = false } = this.props;
const activeCss = active ? 'active' : '';
return (
<div className={`card ${activeCss}`} onClick={this.onClick}>
<h5>{title}</h5>
</div>
);
}
}
class List extends React.Component {
handleClick = () => {
const { onClick, id } = this.props;
onClick(id);
}
onCardClick = cardId => {
const { onCardClick, id: listId } = this.props;
onCardClick({ listId, cardId });
}
render() {
const { title, cards } = this.props;
return (
<div className="list">
<button className="add-card" onClick={this.handleClick}>+</button>
<h4>{title}</h4>
<div>
{
cards.map((card, idx) => {
return (
<Card key={idx} {...card} onClick={this.onCardClick} />
)
})
}
</div>
</div>
);
}
}
class Board extends React.Component {
onAddCard = listId => {
const { onAddCard, id: boardId } = this.props;
const action = {
boardId,
listId
}
onAddCard(action)
}
onCardClick = ({ listId, cardId }) => {
const { onCardClick, id: boardId } = this.props;
const action = {
boardId,
listId,
cardId
}
onCardClick(action)
}
render() {
const { title, list } = this.props;
return (
<div className="board">
<h3>{title}</h3>
{
list.map((items, idx) => {
return (
<List onClick={this.onAddCard} onCardClick={this.onCardClick} key={idx} {...items} />
)
})
}
</div>
);
}
}
const cardRedcer = (state = {}, action) => {
switch (action.type) {
case 'ADD_CARD': {
const { cardId } = action;
return { title: 'new card...', id: cardId }
}
case 'TOGGLE_CARD': {
return {
...state,
active: !state.active
}
}
default:
return state;
}
}
const cardsRedcer = (state = [], action) => {
switch (action.type) {
case 'ADD_CARD':
return [...state, cardRedcer(null, action)];
case 'TOGGLE_CARD': {
return state.map(card => {
if (card.id !== action.cardId) return card;
return cardRedcer(card, action);
});
}
default:
return state;
}
}
const listReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_CARD': {
const { listId } = action;
return state.map(item => {
if (item.id !== listId) return item;
return {
...item,
cards: cardsRedcer(item.cards, action)
}
});
}
case 'TOGGLE_CARD': {
const { listId, cardId } = action;
return state.map(item => {
if (item.id !== listId) return item;
return {
...item,
cards: cardsRedcer(item.cards,action)
}
});
}
default:
return state;
}
}
class App extends React.Component {
state = {
boards: [
{
id: 1,
title: 'Home',
list: [
{
id: 111,
title: 'To Do',
cards: [
{ title: 'Finish this project.', id: 1 },
{ title: 'Start that project.', id: 2 }
]
},
{
id: 222,
title: 'Doing',
cards: [
{ title: 'Finish Another project.', id: 1 },
{ title: 'Ask on StackOverflow.', id: 2 }]
},
{
id: 333,
title: 'Done',
cards: []
}
]
}
]
}
onAddCard = ({ boardId, listId }) => {
const cardId = uuidv4();
this.setState(prev => {
const nextState = prev.boards.map(board => {
if (board.id !== boardId) return board;
return {
...board,
list: listReducer(board.list, { type: 'ADD_CARD', listId, cardId })
}
})
return {
...prev,
boards: nextState
}
});
}
onCardClick = ({ boardId, listId, cardId }) => {
this.setState(prev => {
const nextState = prev.boards.map(board => {
if (board.id !== boardId) return board;
return {
...board,
list: listReducer(board.list, { type: 'TOGGLE_CARD', listId, cardId })
}
})
return {
...prev,
boards: nextState
}
});
}
render() {
const { boards } = this.state;
return (
<div className="board-sheet">
{
boards.map((board, idx) => (
<Board
id={board.id}
key={idx}
list={board.list}
title={board.title}
onAddCard={this.onAddCard}
onCardClick={this.onCardClick}
/>
))
}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
.board-sheet{
padding: 5px;
}
.board{
padding: 10px;
margin: 10px;
border: 1px solid #333;
}
.list{
border: 1px solid #333;
padding: 10px;
margin: 5px;
}
.card{
cursor: pointer;
display: inline-block;
overflow: hidden;
padding: 5px;
margin: 5px;
width: 100px;
height: 100px;
box-shadow: 0 0 2px 1px #333;
}
.card.active{
background-color: green;
color: #fff;
}
.add-card{
cursor: pointer;
float: right;
}
<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>
<div id="root"></div>

ReactJS - setting an inline style equal to a property on state is not working. What's going on?

I'm trying to get a FormWarning to display when users input incorrect information, but it seems to have disappeared on me. I'm trying to control whether or not it displays with this.state.formWarning.display - when the validateInputs function runs, if it determines an input is invalid, it should change the value of display from 'none' to 'block'. I'm trying to set the style for the Component to having a display that matches this.state.formWarning.display, but I am getting an error. Is my belief that you can set the styles for a component inline via an object not correct? Getting bugs regardless. ie
export default class FormOne extends React.Component {
constructor(props) {
super(props)
this.state = {
formOne: {
shippingAddress: {
firstName: '',
lastName: '',
address1: '',
city: '',
state: '',
zip: '',
country: 'US'
},
phone: '',
email: ''
},
formWarning: {
text: '',
invalidInputID: '',
display: 'block'
},
isSubmitted: false,
submitting: false
}
this.styles = this.props.styles || {}
}
componentWillReceiveProps(nextProps) {
if(nextProps.state.stepOne &&
nextProps.state.stepOne.formOneResponse) {
let formOneResponse = nextProps.state.stepOne.formOneResponse
formOneResponse.status === "delayed" || formOneResponse.status === "success"
? this.setState({isSubmitted: true})
: alert(formOneResponse.errorMessage)
this.setState(state => ({submitting: false}))
}
}
validateInputs = (inputs) => {
let { email, phone, shippingAddress } = inputs,
shippingKeys = Object.keys(shippingAddress)
console.log('validate inputs is firing')
for(let i = 0; i < Object.keys(shippingAddress).length; i++) {
let key = shippingKeys[i], input = shippingAddress[key]
if(!input) {
return this.showFormWarning(key)
}
}
if(!phone) return this.showFormWarning('phone')
if(/\S+#\S+\.\S+/.test(email)) return
this.showFormWarning('email')
return true
}
showFormWarning = key => {
clearTimeout(this.warningTimeout)
console.log('showformwarnign is firing')
this.setState(state => ({
formWarning: {
...state.formWarning,
text: 'Please fill out this field',
invalidInputID: key,
display: 'block'
}
}))
this.warningTimeout = setTimeout(() => {
this.setState(state => ({
formWarning: {
...state.formWarning,
display: 'none'
}
}))
}, 5000)
return false
}
saveInputVal = (event) => {
let { formOne: tempFormOne } = this.state,
input = event.currentTarget
console.log('saveinputvals is firing')
if(input.name === 'phone' || input.name === 'email') {
this.setState(state => ({
formOne: {
...state.formOne,
[input.name]: input.value
}
}))
} else {
this.setState(state => ({
formOne: {
...state.formOne,
shippingAddress: {
...state.formOne.shippingAddress,
[input.name]: input.value
}
}
}))
}
}
submit = (event) => {
event.preventDefault()
if(!this.validateInputs(this.state.formOne)) return
this.setState(state => ({submitting: true}))
this.props.saveShippingData(this.state.formOne)
this.props.stepOneSubmit(this.state.formOne)
}
render() {
if (this.state.isSubmitted) return <Redirect to="/order" />
let CustomTag = this.props.labels ? 'label' : 'span',
{ inputs, saveInputVal, styles, state } = this,
{ formWarning, submitting } = state,
{ invalidInputID, text, display } = formWarning
return (
<div style={this.styles.formWrapper}>
{
typeof this.props.headerText === 'string'
? ( <h2 style={this.styles.formHeader}>
{this.props.headerText}</h2> )
: this.props.headerText.map((text) => {
return <h2 key={text} style={this.styles.formHeader}
className={'header'+this.props.headerText.indexOf(text)}>{text}</h2>
})
}
<form onSubmit={this.submit} style={this.styles.form}>
<FormOneInputs inputs={inputs} saveInputVal={saveInputVal}
CustomTag={CustomTag} styles={styles} />
<button style={this.styles.button}>{this.props.buttonText}
</button>
</form>
<Throbber throbberText='Reserving your order...' showThrobber=
{submitting} />
<FormWarning style={display: {this.state.formWarning.display}} invalidInputID={invalidInputID} text={text}/>
</div>
)
}
}
You don't need to set any CSS class. The approach is as follows:
(1) Given a component you want to render or not render depending on a variable
(2) Make a helper method that checks for the condition and returns the actual component if you want it rendered. Otherwise, do nothing (basically returns undefined)
(3) Call that method from wherever you want the component to possibly appear.
Concrete example:
class FormOne extends React.Component {
// (...) all other things omitted to focus on the problem at hand
renderFormWarning() {
if (formIsInvalid) {
return <FormWarning ... />;
}
// else won't do anything (won't show)
}
render() {
return (
{/* ... */}
{this.renderFormWarning()}
);
}
}
In the above example, replace formIsInvalid with some statement that will tell you if the form is invalid. Then, if that condition is true, it will return the FormWarning component. Otherwise, no form warning will be shown. From the render() method, all you need do is call that helper method.

Resources