I'm new to react and as well to the terms of functional, imperative, declarative. And I get to know that pure function is easy to test. I am self taught to program with Javascript. So far, it is working but my goal is to learn to write clean and maintainable code.
my question is the method addProductToSaleList below is bad and untestable because it is imperative? and how can I do it differently.
class SaleComponent extends React.Component {
addProductToSaleList = (values, dispatch, props) => {
//filter product from productList
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
if (productFound) {
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.filter(detail => {
if (productFound.name === detail.product) {
return detail
}
return undefined
})[0]
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
} else {
alert('The product code you add is not exist in product list');
}
}
render() {
// Render saleList
}
}
I belive this question should go to Code Review, but I will give it a shot. Part of the code can be improved
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
First, filter function receives a callback and for each item that callback will be executed. If the callback returns a value interpreted as true, it will return the item in the new array the function will build. Otherwise, it will skip that item. Assuming you're trying to find one item in the code, you could use the function find which will return you that element directly (no need for [0]), or undefined if that item is not found. So your code could be rewrite to
const productFound = props.productList.find(product => values.productCode === product.code.toString());
Note: No IE support.
Then, if the value was not found, you could just alert and do an early return. (You might also want to handle errors differently, with a better format than plain alert).
The code would look like
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// rest of the function
in order to find details, you can use find method as well
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
and then just call the rest of the code
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
Another improvement:
You're receiving a dispatch function as a parameter, but you're not using it. So you could remove it from function's declaration
(values, props) => { ... }
And you could split the last part into two different functions, something like
const getAction = details => `${detailFound ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = detailFound;
return {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
};
}
and then just call
props.dispatcher(getAction(details), getObject(details, productFound));
The end result would look like
addProductToSaleList = (values, props) => {
//filter product from productList
const productFound = props.productList.find(product => values.productCode === product.code.toString());
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
const getAction = details => `${details ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = details;
return {
...rest,
qty: parseInt(details.qty, 10) + 1
};
}
props.dispatcher(getAction(details), getObject(details, productFound));
}
my question is the method addProductToSaleList below is bad and
untestable because it is imperative
Well your code is testable, there are no external dependencies. So you could pass mocked values and props and add unit tests to that. That means, passing a fake values and props (they are just plain js object) and make assertions over that.
For instance:
You could mock dispatcher function and given the fake values in productList and saleItem.details you could see if dispatcher is called with the proper values. You should test different combinations of that
Mock alert function (Again, I would use another UI approach) and verify it is called, and that no other code is called (asserting that your fake dispatcher is not called). Something like this:
let actionToAssert;
let objectToAssert;
let values = { productCode: 'somecode' };
let props = {
productList: // your item listm with id and price, name, etc,
saleItem: {
details: // your details array here
}
dispatcher: (action, newObject) => {
actionToAssert = action;
objectToAssert = newObject;
}
}
addProductToSaleList(values, props); // make here assertions over actionToAssert and objectToAssert
Related
I'm trying to run a function inside a reducer function to calculate a cart total, but the value in the state object is the function and not the result of the function. After render, the result is displayed, but I cannot pass the object to other components (I'm using a context). How do I do it? Here is the code (assume that the values work, because they do).
I've tried setting it to the const to no avail. I've tried an anonymous function that returns the function's total, and it still doesn't work. I've also tried just calling the function.
const reducer = (cart, action) => {
switch(action.type) {
case("ADD_ITEM"):
return {
...cart,
products: {
...cart.products,
[action.payload.product.id]: {...action.payload.product}
},
total: () => (cartTotal)
}
break
case("REMOVE_ITEM"):
delete cart.products[action.payload]
return {
...cart,
products: {
...cart.products
},
total: () => (cartTotal)
}
break
case("CLEAR_CART"):
return {
cart: {
...initialState
}
}
break
}
}
Here is the object:
cart: {
products: [{}],
total: 0
}
Here is the function to return the total:
const cartTotal = () => {
const total = Object.values(cart.products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price
return prev + currPrice
}, 0)
return total.toFixed(2)
}
Right now, I'm passing the method that allows you to calculate the total, but it seems like it is unnecessary, as I'm watching the cart state and updating the value of total each time items are added/removed. How do I set the value of a property inside of the reducer function as the return of another helper function? Thanks!
I don't know why are you storing derived state, well, in state? This should be computed via a selector when reading your state out (and/or passed to a custom Context provider).
If you must store the total in state then you need to call the cartTotal function to be able to store its return value. Unfortunately this will only compute the cart total on the unupdated cart since you are currently in the function that returns the new cart state.
You can factor out the cart update so you have an updated cart products object, and with a small revision of cartTotal it can consume this updated cart products object and compute a total.
Example:
const cartTotal = (products) => {
const total = Object.values(products).reduce((prev, curr) => {
const currPrice = (curr.data.on_sale && curr.data.sale_price) ? curr.data.sale_price : curr.data.price;
return prev + currPrice;
}, 0);
return total.toFixed(2);
};
Cases
case "ADD_ITEM": {
const { payload } = action;
const products = {
...cart.products,
[payload.product.id]: { ...payload.product },
}
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
case "REMOVE_ITEM": {
const products = { ...cart.products };
delete products[action.payload];
return {
...cart,
products,
total: cartTotal(products),
}
break;
}
You need to call cartTotal to calculate the value.
total: cartTotal()
I have state set as follow
const [stories, setStories] = useState([]);
I fetch Data from API in array, and i map the array and set the using setStories as:
setStories(prevState => prevState.concat({user: {name: 'XYZ', profile: 'ABC', stories: [{id: 1, image: 'testing'}];
The above codes are working fine, but i am stuck, when i have to concat the latest story if the id did not matched with fetched data. I have tried below solution but it didnot help:
stories.map(story => {
if(story && story.hasOwnProperty(key)){
//where above key is the user key fetched from the another API, i.e., user key
story?.[key].stories.map(storedStory =>
id(storedStory.id !== fetchedStory.id){
story?.[key].stories.concat({story})}
but the above code did not work, as it only mutate the state and is avoiding re-rendering.
Looking for a clean and efficient method to overcome this. THanks
It's hard to tell what you're trying to accomplish without seeing a full example. But I think your main problem is that you're not using the returned value from map, and from the naming it looks like you're appending the wrong element.
It will help to simplify first.
const newState = stories.map(story => {
if (story?.hasOwnProperty(key)) {
const found = story[key].stories.find(s => s.id === fetchedStory.id);
if (found) {
return story;
} else {
// Let's make a new object with the fetchedStory
// appended into THIS user's stories
return {
...story,
[key]: {
...story[key],
stories: [
...story[key].stories,
// This is supposed to be fetchedStory
// not `story` right??
fetchedStory,
]
}
}
}
} else {
return story;
}
});
setStory(newState);
Edit: You're having a hard time expressing your business logic, and the complexity of the data structure is not helping. So keep simplifying, encapsulate the complex syntax into functions then express your business logic plainly. Ie,
const appendStory = (originalObject, userId, storyToAppend) => {
return {
...originalObject,
[userId]: {
...originalObject[userId],
stories: [
...originalObject[userId].stories,
storyToAppend,
]
}
}
};
const userExistsInList = (users, user) => {
return users?.hasOwnProperty(user);
}
const newStoryAlreadyInStories = (stories, newStory) => {
return stories.find(s => s.id === newStory.id);
}
const newState = stories.map(story => {
if (userExistsInList(story, key)) {
const found = newStoryAlreadyInStories(story[key].stories, fetchedStory);
if (found) {
// User is already in state and the new story is already in the list
// Add business logic here
} else {
// User is already in state and the new story
// is not in their list
// Add business logic here
}
} else {
// User is not in the list yet
// Add business logic here
}
});
I am trying to test a React component which uses one of the overloads for setState, but am unsure how to assert the call correctly. An example component would be:
class CounterComponent extends React.Component {
updateCounter() {
this.setState((state) => {
return {
counterValue: state.counterValue + 1
};
});
}
}
The assumption here is that this method will be called asyncronously, so cannot rely on the current state, outwith the call to setState (as it may change before setState executes). Can anyone suggest how you would assert this call? The following test fails as it is simply comparing the function names.
it("Should call setState with the expected parameters", () => {
const component = new CounterComponent();
component.setState = jest.fn(() => {});
component.state = { counterValue: 10 };
component.updateCounter();
const anonymous = (state) => {
return {
counterValue: state.counterValue + 1
};
};
//expect(component.setState).toHaveBeenCalledWith({ counterValue: 11 });
expect(component.setState).toHaveBeenCalledWith(anonymous);
});
Edit: Given yohai's response below, i will add some further context as I feel i may have over simplified the problem however i do not want to re-write the entire question for clarity.
In my actual component, the state value being edited is not a simple number, it is an array of objects with the structure:
{ isSaving: false, hasError: false, errorMessage: ''}
and a few other properties. When the user clicks save, an async action is fired for each item in the array, and then the corresponding entry is updated when that action returns or is rejected. As an example, the save method would look like this:
onSave() {
const { myItems } = this.state;
myItems.forEach(item => {
api.DoStuff(item)
.then(response => this.handleSuccess(response, item))
.catch(error => this.handleError(error, item));
});
}
The handle success and error methods just update the object and call replaceItem:
handleSuccess(response, item) {
const updated = Object.assign({}, item, { hasSaved: true });
this.replaceItem(updated);
}
handleError(error, item) {
const updated = Object.assign({}, item, { hasError: true });
this.replaceItem(updated);
}
And replaceItem then replaces the item in the array:
replaceItem(updatedItem) {
this.setState((state) => {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
return {
myItems: working
};
});
}
replaceItem is the method I am trying to test, and am trying to validate that it calls setState with the correct overload and a function which correctly updated the state.
My answer below details how I have solved this for myself,but comments and answers are welcome =)
#Vallerii: Testing the resulting state does seem a simpler way, however if i do, there is no way for the test to know that the method is not doing this:
replaceItem(updatedItem) {
const { myItems } = state;
const working = [...myItems];
const itemToReplace = working.find(x => x.id == updatedItem.id);
if (itemToReplace) {
working.splice(working.indexOf(itemToReplace), 1, updatedItem);
};
this.setState({ myItems: working });
}
When replaceItem does not use the correct overload for setState, this code fails when called repeatedly as (I assume) react is batching updates and the state this version uses is stale.
I think you should test something a little bit different and it will look somthing like this (I'm using enzyme):
import React from 'react'
import { mount } from 'enzyme'
import CounterComponent from './CounterComponent'
it("Should increase state by one", () => {
const component = mount(<CounterComponent />)
const counter = 10;
component.setState({ counter });
component.instance().updateCounter();
expect(component.state().counter).toEqual(counter + 1);
});
I have come up with a solution to this after some further thought. I am not sure it is the best solution, but given that the updateCounter method in the example above passes a function into the setState call, I can simply get a reference to that function, execute it with a known state and check the return value is correct.
The resulting test looks like this:
it("Should call setState with the expected parameters", () => {
let updateStateFunction = null;
const component = new CounterComponent();
component.setState = jest.fn((func) => { updateStateFunction = func;});
component.updateCounter();
const originalState = { counterValue: 10 };
const expectedState = { counterValue: 11};
expect(component.setState).toHaveBeenCalled();
expect(updateStateFunction(originalState)).toEqual(expectedState);
});
i'm having hard time figure out this. Have component which is search filter and pushes all selected filters into url. Everything works like it should except in case of refresh, in that case reducer is updated for selected filter with array with single item in which i have all selected items, not spreaded into array.
f.e. i have url
myexampleapp.com/alltrips?tripType=short_walk,cycling,downhill_cycling,long_walks&season=spring,summer,alle,vinter&lengthTo=50
my reducer
// ------------------------------------
// Constants
// ------------------------------------
export const UPDATE_FILTERS = 'UPDATE_FILTERS';
// ------------------------------------
// Actions
// ------------------------------------
const updateFilter = (key, value) => ({
type: UPDATE_FILTERS,
payload: {
key,
value
}
});
// ------------------------------------
// Action creators
// ------------------------------------
export const updateFilterState = (key, value) => {
return dispatch => {
dispatch(updateFilter(key, value));
};
};
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
tripType: [],
season: [],
tripsTo: undefined,
tripsFrom: undefined
};
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const key = action.payload.key;
const value = action.payload.value;
if (key === 'tripsFrom' || key === 'tripsTo') {
return Object.assign({}, state, { [key]: value });
} else {
var newFilter = state[key].slice();
var ttIdx = state[key].indexOf(value);
if (ttIdx !== -1) {
newFilter.splice(ttIdx, 1);
} else {
newFilter.push(value);
}
}
console.log(newFilter);
return Object.assign({}, state, { [key]: newFilter });
}
default:
return state;
}
}
console.log returns array with 1 element in which have array with 5 elements. but i want that 5 ekements to be in parrent array.
and i'm parsing URL
componentDidMount () {
let {
location: { search },
updateFilterState
} = this.props;
search = search.slice(1);
var queries = search.split('&');
queries.forEach(q => {
var tmp = q.split('=');
if (tmp[0] && tmp[1]) {
if (tmp[0].toLowerCase() === 'triptype') {
updateFilterState(tmp[0], tmp[1].split(','));
console.log(tmp[1].split(','));
} else if (tmp[0].toLowerCase() === 'tripsto') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'tripsfrom') {
updateFilterState(tmp[0], tmp[1]);
} else if (tmp[0].toLowerCase() === 'season') {
updateFilterState(tmp[0], tmp[1].split(','));
}
}
});
this.updateQuery(this.props);
}
So everything works except when i want to refresh.
Pretty new with all this, and been stuck for almost 3 days with this. Hope you understand what im trying to ask here as i'm pretty new and non-english speaker, so i don't know all the terms so i can better express myself. Can someone give me some pointers?
If I'm not mistaken you are feeding the reducer with an array for season and tripType. So, when you try to update those values, you are not actually spreading that array. This is your value parameter. Hence, if you do this you will have a parent array with your desired result:
newFilter.push(...value);
... is ES6's spread syntax. So we are spreading our array and pushing it into our newFilter.
But again if I don't see it wrong you will have problems with this code since you are not checking the existence of your values right. You are looking indexOf something but if you really feeding your reducer with an array, for which one you are looking this index?
Here is a cleaner way of doing this if I'm not mistaken what you are trying to do here:
export function filterReducer (state = initialState, action) {
switch (action.type) {
case UPDATE_FILTERS: {
const { key, value } = action.payload;
if (key === 'tripsFrom' || key === 'tripsTo') {
return { ...state, [key]: value };
}
const newFilter = Array.isArray(value)
? [ ...new Set( [ ...state[key], ...value ] ) ]
: [ ...new Set( [ ...state[key], value ] ) ];
return { ...state, [key]: newFilter};
}
default:
return state;
}
}
Some differences with your code:
I am using spread syntax instead of Object.assign.
Instead of checking all the existence values (iterating the array and doing some logic) I'm using here Set object. It creates an object of unique values of what we give it. So I am cheating here and spreading our old state with spreading our value into an array, give this to our Set, and again at the top level spreading it again into an array. If you don't do the last spread you will get an object but here we want an array.
I am trying to implement a queue wherein the user can switch items from one list to another. i.e. from "available" to "with client", where the state of the queue is held in the root React component like so:
this.state = {
queue: {
available: [{ name: "example", id: 1 }, ...],
withClient: [],
unavailable: []
}
};
However my move function is broken:
move(id, from, to) {
let newQueue = deepCopy(this.state.queue);
console.log("NEW QUEUE FROM MOVE", newQueue); // { [from]: [], [to]: [undefined] }
console.log("QUEUE IN STATE FROM MOVE", this.state.queue); // { [from]: [{...}], [to]: [] }
let temp = newQueue[from].find(x => x.id === id);
newQueue[from] = this.state.queue[from].filter(x =>
x.id !== id
);
newQueue[to] = this.state.queue[to].concat(temp);
this.setState({
queue: newQueue
});
}
I am expecting the 2 console.logs to be the same. There seems to be some sort of race condition happening here that I am not understanding. It results in getting an error Cannot read property 'id' of undefined
Right now the only way the movement is triggered from a HelpedButton component contained within each item in the "Available" list which gets passed props:
class HelpedButton extends React.Component {
constructor() {
super();
this.clickWrapper = this.clickWrapper.bind(this);
}
clickWrapper() {
console.log("I have id right?", this.props.id); //outputs as expected
this.props.move(this.props.id, "available", "withClient");
}
render() {
return (
<span style={this.props.style}>
<button onClick={this.clickWrapper}>
<strong>Helped A Customer</strong>
</button>
</span>
);
}
}
export default HelpedButton;
I don't think there's anything wrong with the deepCopy function, but here it is, imported file from node_modules:
"use strict";
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
module.exports = deepCopy;
The recommended way by react documentation to make setState which depends on previous state is to use the updater form which looks like this setState((prevState,prevProp)=>{}) . With this method your move function will look like this.
move(id, from, to) {
let temp = newQueue[from].find(x => x === x.id);
newQueue[from] = this.state.queue[from].filter(x =>
x.id !== id
);
newQueue[to] = this.state.queue[to].concat(temp);
this.setState((prevState)=>{
queue: {
...prevState.queue,
[from]: prevState.queue[from](o => x.id !== id),
[to]: [from]: prevState.queue[from].concat(prevState.queue[from].find(x => x === x.id))
}
});
}
I believe the reason why you see different outputs is console.log outputs a live object which means if you run console.log(obj) and later change obj param the displayed params are changes. Try to debug with console.log("obj: " + JSON.strignify(obj)).
Here is more about why you should call the async method of setState when relying on previous state in react docs