I have a rather simple requirement which is turning out to be quite complex for me. I'm developing a basic todo app with following UI:
Design
Now I need to update the array of object such that only the text of specific item should be updated. Here is my attempt but it just adds a new component on every key press:
import React, {useState} from "react";
const DynamicInput = () => {
const [todos, setTodos] = useState([])
const onAddClick = () => {
setTodos(prevState => {
return [...prevState, {id: prevState.length + 1, text: "", up: "↑", down: "↓", del: "x"}]
})
}
const onValueUpdate = (id) => (event) => {
let tempObject = todos[id]
setTodos(prevState => {
return [...prevState, {
id: id,
text: event.target.value,
up: "Up",
down: "Down",
del: "x"
}];
})
}
const onUpArrow = (event) => {
console.log("On up")
}
const onDownArrow = (event) => {
console.log("On down")
}
const onDeleteArrow = (event) => {
console.log("On delete")
}
return (
<>
<button onClick={onAddClick}>+</button>
{todos.map(todo => {
return(
<div key={todo.id}>
<input onChange={onValueUpdate(todo.id)} value={todo.text}></input>
<button onClick={onUpArrow}>{todo.up}</button>
<button onClick={onDownArrow}>{todo.down}</button>
<button onClick={onDeleteArrow}>{todo.del}</button>
</div>)
})}
</>
);
};
export default DynamicInput;
To simply solve your problem you can change your onValueUpdate() method to this :
const onValueUpdate = (id) => (event) => {
setTodos(prevState => {
let data = [...prevState];
let indexOfTodo = data.findIndex(todo => todo.id === id);
data[indexOfTodo] = {
...data[indexOfTodo],
text: event.target.value,
};
return data;
});
};
I am going through documentation for Algolia and Next and am trying to get URLs to show up in the address bar, most of the examples are as Class Components but the app I am working on uses Hooks. I am trying to test some of the examples on my site, but am stuck on how to correctly convert a class app in React to hooks as I keep getting errors.
Class Example:
const updateAfter = 400;
const createURL = (state) => `?${qs.stringify(state)}`;
const searchStateToUrl = (props, searchState) =>
searchState ? `${props.location.pathname}${createURL(searchState)}` : '';
const urlToSearchState = ({ search }) => qs.parse(search.slice(1));
class App extends Component {
state = {
searchState: urlToSearchState(this.props.location),
lastLocation: this.props.location,
};
static getDerivedStateFromProps(props, state) {
if (props.location !== state.lastLocation) {
return {
searchState: urlToSearchState(props.location),
lastLocation: props.location,
};
}
return null;
}
onSearchStateChange = searchState => {
clearTimeout(this.debouncedSetState);
this.debouncedSetState = setTimeout(() => {
const href = searchStateToURL(searchState);
this.props.router.push(href, href, {
shallow: true
});
}, updateAfter);
this.setState({ searchState });
};
My converted attempt:
const createURL = state => `?${qs.stringify(state)}`;
const pathToSearchState = path =>
path.includes("?") ? qs.parse(path.substring(path.indexOf("?") + 1)) : {};
const searchStateToURL = searchState =>
searchState ? `${window.location.pathname}?${qs.stringify(searchState)}` : "";
const DEFAULT_PROPS = {
searchClient,
indexName: "instant_search"
};
const Page = () => {
const [searchState, setSearchState] = useState(<not sure what goes here>)
const [lastRouter, setRouterState] = useState(router)
Page.getInitialProps = async({ asPath }) => {
const searchState = pathToSearchState(asPath);
const resultsState = await findResultsState(App, {
...DEFAULT_PROPS,
searchState
});
return {
resultsState,
searchState
};
}
//unsure how to convert here
static getDerivedStateFromProps(props, state) {
if (!isEqual(state.lastRouter, props.router)) {
return {
searchState: pathToSearchState(props.router.asPath),
lastRouter: props.router
};
}
return null;
}
const onSearchStateChange = searchState => {
clearTimeout(debouncedSetState);
const debouncedSetState = setTimeout(() => {
const href = searchStateToURL(searchState);
router.push(href, href, {
shallow: true
});
}, updateAfter);
setSearchState({ searchState });
};
So in my program, I am statically creating and removing players. However, the editing of a players's attribute isn't working.
I have 3 action types. ADD_PLAYER, REMOVE_PLAYER, EDIT_PLAYER
For each one of them, I create an action generator, which contains a type and any other parameters accordingly. Here is my full code
import { createStore, combineReducers } from 'redux';
import uuid from 'uuid';
// ADD_PLAYER
const addPlayer = (
{
firstName = '',
lastName = '',
age = 0,
position = ''
} = {}) => ({
type: 'ADD_PLAYER',
player: {
id: uuid(),
firstName,
lastName,
age,
position
}
});
// REMOVE_PLAYER
const removePlayer = ( {id} = {} ) => ({
type: 'REMOVE_PLAYER',
id
});
// EDIT_PLAYER
const editPlayer = (id, updates) => ({
type: 'EDIT_PLAYER',
id,
updates
})
const playersReduxDefaultState = [];
// The Player reducer
const playersReducer = (state = playersReduxDefaultState, action) => {
switch(action.type) {
case 'ADD_PLAYER':
return [
...state,
action.player
]
case 'REMOVE_PLAYER':
return state.filter(({id}) => id !== action.id)
case 'EDIT_PLAYER':
return state.map((player) => {
if(player.id === action.id) {
return {
...player,
...action.updates
}
} else {
return player
}
});
default:
return state;
}
};
const store = createStore(
combineReducers({
players: playersReducer
})
)
store.subscribe(() => {
console.log(store.getState())
});
store.dispatch(
addPlayer(
{
firstName: 'Theo',
lastName: 'Tziomakas',
age: 38,
position: 'Striker'
}
))
const playerOne = store.dispatch(
addPlayer(
{
firstName: 'Vasilis',
lastName: 'Tziastoudis',
age: 38,
position: 'Defender'
}
))
store.dispatch(removePlayer({ id: playerOne.player.id}));
// Edit player's one age.
store.dispatch(editPlayer(playerOne.player.id, { age: 29 }));
What am I missing?
Thanks,
Theo.
In my project I'm using tcomb-form-native library to validation a form. Redux working fine but I can't pass value of inputs to reducer. I have to do this, because I want to create array with data from fields.
How can I pass values of my inputs to reducer?
Or maybe it's not possible with this library and I have to use another one?
Form.js
const mapDispatchToProps = dispatch => {
return {
closeExpenseDialog: (value) => dispatch({type: 'CLOSE_EXPENSE_DIALOG'}),
};
};
const mapStateToProps = state => {
return {
value: state.closeExpenseDialog.value,
};
};
const Form = t.form.Form;
const Expense = t.struct({
expense: t.String,
cost: t.Number
});
const options = {
fields: {
expense: {
error: 'This field is required'
},
cost: {
error: 'This field is required'
}
}
};
handleClick = () => {
const value = this._form.getValue();
if (value) {
console.log(value);
this.props.closeExpenseDialog(value);
} else {
console.log('validation failed');
}
}
<Form
type={Expense}
ref={c => this.props._form = c}
options={options}
value={this.props.value}
/>
<ActionButton
onPress={this.props.closeExpenseDialog}
title={title}
/>
Reducer.js
const initialState = {
value: {}
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case 'CLOSE_EXPENSE_DIALOG':
console.log('it works')
console.log(state.value) //undefined
default:
return state;
}
};
I needed add onChange() attribute and use it to pass object value to the Reducer.
Form.js
const Expense = t.struct({
expense: t.String,
cost: t.Number
});
const options = {
fields: {
expense: {
error: 'This field is required'
},
cost: {
error: 'This field is required'
}
}
};
handleClick = () => {
const value = this._form.getValue();
if (value) {
this.props.submitExpenseDialog();
}
}
<Form
type={Expense}
ref={c => this._form = c}
options={options}
value={this.props.value}
onChange={this.props.changeExpenseInputs}
/>
<ActionButton
onPress={this.handleClick}
title={title}
/>
Reducer.js
const initialState = {
value: {}
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case 'SUBMIT_EXPENSE_DIALOG':
console.log(state.value)
default:
return state;
}
};
I am learning Redux at school, as such we are using tests to insure we have benchmarks passing to help us in our understanding of the building blocks.
I am up to the portion where I am creating the Reducerfunction and I am almost done \o/ however I can't get one test to pass.
1) returns the initial state by default
And below the console spits back...
Reducer returns the initial state by default:
AssertionError: expected undefined to be an object
at Context. (tests/redux.spec.js:103:49)
My thinking it's because the test handles some of the concerns one would be responsible for e.g importing, creating action types etc. But not all. So maybe I am missing something the test is not providing?
Anyway here is my reducer file:
import pet from "../components/PetPreview";
import { createStore } from "redux";
import { adoptPet, previewPet, addNewDog, addNewCat } from "./action-creators";
// ACTION TYPES
const PREVIEW_PET = "PREVIEW_PET";
const ADOPT_PET = "ADOPT_PET";
const ADD_NEW_DOG = "ADD_NEW_DOG";
const ADD_NEW_CAT = "ADD_NEW_CAT";
// INTITIAL STATE
const initialState = {
dogs: [
{
name: "Taylor",
imgUrl: "src/img/taylor.png"
},
{
name: "Reggie",
imgUrl: "src/img/reggie.png"
},
{
name: "Pandora",
imgUrl: "src/img/pandora.png"
}
],
cats: [
{
name: "Earl",
imgUrl: "src/img/earl.png"
},
{
name: "Winnie",
imgUrl: "src/img/winnie.png"
},
{
name: "Fellini",
imgUrl: "src/img/fellini.png"
}
]
// These dogs and cats are on our intial state,
// but there are a few more things we need!
};
export default function reducer(prevState = initialState, action) {
var newState = Object.assign({}, prevState)
console.log('initialState', typeof initialState)
switch (action.type) {
case PREVIEW_PET:
// console.log('newState', newState)
return Object.assign({}, prevState, {
petToPreview: action.pet
});
break
case ADOPT_PET:
return Object.assign({}, prevState, {
petToAdopt: action.pet
});
break
case ADD_NEW_DOG:
// console.log('action', action.dog)
// console.log('prevState.dogs', prevState.dogs)
newState.dogs = prevState.dogs.concat([action.dog])
return newState;
break
case ADD_NEW_CAT:
// console.log('action', action.dog)
// console.log('prevState.dogs', prevState.dogs)
newState.cats = prevState.cats.concat([action.cat])
return newState;
break;
default:
return prevState;
}
return initialState
}
As you can see after the switch block I am returning the initialState
Shouldn't that be it?
Below is the redux.spec.js file:
import { expect } from "chai";
import { createStore } from "redux";
// You will write these functions
import {
previewPet,
adoptPet,
addNewDog,
addNewCat
} from "../src/store/action-creators";
import reducer from "../src/store/reducer";
const DOGS = [
{
name: "Taylor",
imgUrl: "src/img/taylor.png"
},
{
name: "Reggie",
imgUrl: "src/img/reggie.png"
},
{
name: "Pandora",
imgUrl: "src/img/pandora.png"
}
];
const CATS = [
{
name: "Earl",
imgUrl: "src/img/earl.png"
},
{
name: "Winnie",
imgUrl: "src/img/winnie.png"
},
{
name: "Fellini",
imgUrl: "src/img/fellini.png"
}
];
function getRandomPet(pets) {
return pets[Math.floor(Math.random() * pets.length)];
}
describe("Action creators", () => {
describe("previewPet", () => {
it("returns properly formatted action", () => {
const pet = getRandomPet(DOGS);
expect(previewPet(pet)).to.be.deep.equal({
type: "PREVIEW_PET",
pet: pet
});
});
});
describe("adoptPet", () => {
it("returns properly formatted action", () => {
const pet = getRandomPet(DOGS);
expect(adoptPet(pet)).to.be.deep.equal({
type: "ADOPT_PET",
pet: pet
});
});
});
describe("addNewDog", () => {
it("returns properly formatted action", () => {
const pet = getRandomPet(DOGS);
expect(addNewDog(pet)).to.be.deep.equal({
type: "ADD_NEW_DOG",
dog: pet
});
});
});
describe("addNewCat", () => {
it("returns properly formatted action", () => {
const pet = getRandomPet(CATS);
expect(addNewCat(pet)).to.be.deep.equal({
type: "ADD_NEW_CAT",
cat: pet
});
});
});
}); // end Action creators
describe("Reducer", () => {
let store;
beforeEach("Create the store", () => {
// creates a store (for testing) using your (real) reducer
store = createStore(reducer);
});
it("returns the initial state by default", () => {
// In addition to dogs and cats, we need two more fields
expect(store.getState().petToPreview).to.be.an("object");
expect(store.getState().petToAdopt).to.be.an("object");
});
describe("reduces on PREVIEW_PET action", () => {
it("sets the action's pet as the petToPreview on state (without mutating the previous state)", () => {
const prevState = store.getState();
const pet = getRandomPet(DOGS);
const action = {
type: "PREVIEW_PET",
pet: pet
};
store.dispatch(action);
const newState = store.getState();
// ensures the state is updated properly - deep equality compares the values of two objects' key-value pairs
expect(store.getState().petToPreview).to.be.deep.equal(pet);
// ensures we didn't mutate anything - regular equality compares the location of the object in memory
expect(newState.petToPreview).to.not.be.equal(prevState.petToPreview);
});
});
describe("reduces on ADOPT_PET action", () => {
it("sets the action's pet as the petToAdopt on state (without mutating the previous state)", () => {
const prevState = store.getState();
const pet = getRandomPet(DOGS);
const action = {
type: "ADOPT_PET",
pet: pet
};
store.dispatch(action);
const newState = store.getState();
expect(newState.petToAdopt).to.be.deep.equal(pet);
expect(newState.petToAdopt).to.not.be.equal(prevState.petToAdopt);
});
});
describe("reduces on ADD_NEW_DOG action", () => {
it("adds the new dog to the dogs array (without mutating the previous state)", () => {
const prevState = store.getState();
const pet = getRandomPet(DOGS);
const action = {
type: "ADD_NEW_DOG",
dog: pet
};
store.dispatch(action);
const newState = store.getState();
expect(newState.dogs.length).to.be.equal(prevState.dogs.length + 1);
expect(newState.dogs[newState.dogs.length - 1]).to.be.deep.equal(pet);
expect(newState.dogs).to.not.be.equal(prevState.dogs);
});
});
describe("reduces on ADD_NEW_CAT action", () => {
it("adds the new cat to the cats array (without mutating the previous state)", () => {
const prevState = store.getState();
const pet = getRandomPet(CATS);
const action = {
type: "ADD_NEW_CAT",
cat: pet
};
store.dispatch(action);
const newState = store.getState();
expect(newState.cats.length).to.be.equal(prevState.cats.length + 1);
expect(newState.cats[newState.cats.length - 1]).to.be.deep.equal(pet);
expect(newState.cats).to.not.be.equal(prevState.cats);
});
});
describe("handles unrecognized actions", () => {
it("returns the previous state", () => {
const prevState = store.getState();
const action = {
type: "NOT_A_THING"
};
store.dispatch(action);
const newState = store.getState();
// these should be the same object in memory AND have equivalent key-value pairs
expect(prevState).to.be.an("object");
expect(newState).to.be.an("object");
expect(newState).to.be.equal(prevState);
expect(newState).to.be.deep.equal(prevState);
});
});
}); // end Reducer
Thanks in advance!
in the test cases, one of the test case default says
it("returns the initial state by default", () => {
// In addition to dogs and cats, we need two more fields
expect(store.getState().petToPreview).to.be.an("object");
expect(store.getState().petToAdopt).to.be.an("object");
});
meaning there must be petTpPreview and petToAdapt protery attached to the store at the inital itself. this can be done by adding these two as boject to the state as follows.
// INTITIAL STATE
const initialState = {
petToPreview:{},
petToAdopt: {},
dogs: [
{
name: "Taylor",
imgUrl: "src/img/taylor.png"
},
{
name: "Reggie",
imgUrl: "src/img/reggie.png"
},
{
name: "Pandora",
imgUrl: "src/img/pandora.png"
}
],
cats: [
{
name: "Earl",
imgUrl: "src/img/earl.png"
},
{
name: "Winnie",
imgUrl: "src/img/winnie.png"
},
{
name: "Fellini",
imgUrl: "src/img/fellini.png"
}
]
// These dogs and cats are on our intial state,
// but there are a few more things we need!
};
hope it helps!
All paths in the switch statement lead to a return, which means your return initialState on the penultimate line is unreachable.
Also, your newState is nothing but a clone of prevState, and is unnecessary.
Removing that, and adding a helper function for switchcase combined with some es6 spread love, your code becomes
const switchcase = cases => defaultValue => key =>
(key in cases ? cases[key] : defaultValue);
const reducer = (state = initialState, action) =>
switchcase({
[PREVIEW_PET]: { ...state, petToPreview: action.pet },
[ADOPT_PET]: { ...state, petToAdopt: action.pet },
[ADD_NEW_DOG]: { ...state, dogs: [...state.dogs, action.dog] },
[ADD_NEW_CAT]: { ...state, cats: [...state.cats, action.cat] },
})(state)(action.type);
With all the clutter gone, it's glaringly obvious that the problem is in the fact that your code returns the initialState object if action.type === undefined. And your initialState object contains only dogs and cats properties, whereas your test expects there to be petToPreview and petToAdopt properties.
You can add those properties in the initialState or you can change the test depending on what functionality you want.
Shouldn't you return the previous state by default? The by default is the case that the reducer doesn't care about the action, and simply return its current state, which is prevState in your case.