How to test react component with custom hook - reactjs

I'm trying to renders items, but due to a custom hook (useBicycles), the map method passes through an empty array. How i can to test this?
My component:
function Items(props: Props) {
const { bicycles } = props;
const filteredBicycles = useBicycles(bicycles);
return (
<div className="items" data-testid="items">
{filteredBicycles.map((item) => {
return <Item item={item} key={uuidv4()} />;
})}
</div>
);
}
My test:
test('renders itself with data without filters', async () => {
renderWithRedux(<Items bicycles={fakeData} isLoading={false} />, FakeInitialState);
const itemsElem = screen.getByTestId('items');
expect(itemsElem).toBeInTheDocument();
const items = await screen.findAllByTestId('item'); <<< ERROR NOT FOUNDED ITEM
expect(items.length).toEqual(QUANTITY_PRODUCTS);
});
Custom hook useBicycles:
It should return filtered array based on the values from store but result in the test I get empty array. If I remove the hook call in the component then everything renders normally.
export const useBicycles = (bicycles: IBicycle[]) => {
const { searchValue } = useAppSelector((state) => state.searchReducer);
const { sortOption } = useAppSelector((state) => state.sortReducer);
let filteredBicycles = bicycles;
// search
filteredBicycles = useMemo(() => {
return bicycles.filter((item) => {
return item.name.toLowerCase().includes(searchValue.toLowerCase());
});
}, [searchValue, bicycles]);
// sorting
filteredBicycles = useMemo(() => {
switch (sortOption) {
case SortOptions.name_asc:
return [...filteredBicycles].sort((a, b) => a.name.localeCompare(b.name));
case SortOptions.name_desc:
return [...filteredBicycles].sort((a, b) => b.name.localeCompare(a.name));
case SortOptions.price_asc:
return [...filteredBicycles].sort((a, b) => a.price - b.price);
case SortOptions.price_desc:
return [...filteredBicycles].sort((a, b) => b.price - a.price);
case SortOptions.quantity_asc:
return [...filteredBicycles].sort((a, b) => a.quantity - b.quantity);
case SortOptions.quantity_desc:
return [...filteredBicycles].sort((a, b) => b.quantity - a.quantity);
default:
return filteredBicycles;
}
}, [sortOption, filteredBicycles]);
return filteredBicycles;
};

Related

My useState hook is not updating itself and when i am trying to get data using filter, its not working

When I am trying to get data from an array using filter and find, it's not getting filtered also the _ids are the same when I cross-checked the array, also useState is also not updating
1. How should I filter one element from an array, Am I doing this right?
2. useState is not working, not updating data
I am getting every data from context (c1)
sd is returning array of single object, so to get one first index I am returning sd[0]
const ReadTemplate = (props) => {
const c1 = useContext(PostsContext);
const [first, myData] = useState({});
const first_load_func = () => {
const id = props.match.params.id;
const sd = c1.data.filter((c1) => id === c1._id);
const business_props = c1.business_data.filter((c1) => id === c1._id);
const startups_props = c1.startups_data.filter((c1) => id === c1._id);
const tech_props = c1.tech_data.filter((c1) => id === c1._id);
const sports_props = c1.sports_data.filter((c1) => id === c1._id);
if (sd) {
return sd[0];
} else if (business_props) {
return business_props[0];
} else if (startups_props) {
return startups_props[0];
} else if (tech_props) {
return tech_props[0];
} else if (sports_props) {
return sports_props[0];
} else {
return <MyAlert />;
}
};
const func = (data) => {
if (data) {
setTimeout(() => {
myData(data);
}, 1000);
console.log('ye first hai');
console.log(first._id);
console.log('ye data hai');
console.log(data);
} else {
console.log('No');
}
};
useEffect(() => {
first_load_func();
func(first_load_func());
}, [first]);
return (
<>
<PostDesign props={first} />
</>
);
};
export default ReadTemplate;
My guess from your code is that you should assign the filtered data when the component is rendered, not when first changes:
useEffect(() => {
func(first_load_func());
}, []);
It may be useful to convert ids toString() before comparing them:
const sd = c1.data.filter((c1) => id.toString() === c1._id.toString());

How to map data from Axios in React Native?

I'm using Axios to get data from an API.
I want to loop through all the items in a feed fetch with Axios using map function and get there title. But I don't know how to map it because my feed is structure like items[0].title, items[1].title...
const URL = 'https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Fvnexpress.net%2Frss%2Fthe-gioi.rss';
let content = null;
const [feed, setFeed] = useState(null);
useEffect(() => {
axios.get(URL).then((response) => {
setFeed(response.data);
});
}, [URL]);
if(feed) {
/// To get title of first item: content = <Text>{feed.items[0].title}
content =
feed.map(f => {
return <Text>{f.items[???].title}</Text> /// How to loop through all the items[0].title, items[1].title, items[2].title ?
})
}
return (<View>{content}</View>);
EDIT;
If you got array of Feed
const URL = 'https://api.rss2json.com/v1/api.json?rss_url=https%3A%2F%2Fvnexpress.net%2Frss%2Fthe-gioi.rss';
let content = null;
const [feed, setFeed] = useState(null);
useEffect(() => {
axios.get(URL).then((response) => {
setFeed(response.data);
});
}, [URL]);
if(feed) {
/// To get title of first item: content = <Text>{feed.items[0].title}
content = () => {
feed.map((f) => {
return (
<View>
{feed.items.map((item) => {
return <Text>{item.title}</Text>;
})}
</View>
);
});
};
}
return (<View>{content && content()}</View>);
const content = () => {
feeds.map((f) => {
return (
<View>
{feeds.items.map((item) => {
return <Text>{item.title}</Text>;
})}
</View>
);
});
};
If always you get a single Feed
content =
(<View>{feed.items.map(item)=> {
return<Text>{item.title}</Text>
}}</View>)

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

React hook useEffect failed to read new useState value that is updated with firebase's firestore realtime data

I have an array of data object to be rendered. and this array of data is populated by Firestore onSnapshot function which i have declared in the React hook: useEffect. The idea is that the dom should get updated when new data is added to firestore, and should be modified when data is modified from the firestore db.
adding new data works fine, but the problem occurs when the data is modified.
here is my code below:
import React, {useState, useEffect} from 'react'
...
const DocList = ({firebase}) => {
const [docList, setDocList] = useState([]);
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(docList => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...docList]);
console.log('document added: ', docSnap.doc.data());
} // this works fine
if (docSnap.type === 'modified') {
console.log('try docList from Lists: ', docList); //this is where the problem is, this returns empty array, i don't know why
console.log('document modified: ', docSnap.doc.data()); //modified data returned
}
})
})
return () => {
unSubListener();
}
}, []);
apparently, i know the way i declared the useEffect with empty deps array is to make it run once, if i should include docList in the deps array the whole effect starts to run infinitely.
please, any way around it?
As commented, you could have used setDocList(current=>current.map(item=>..., here is working example with fake firebase:
const firebase = (() => {
const createId = ((id) => () => ++id)(0);
let data = [];
let listeners = [];
const dispatch = (event) =>
listeners.forEach((listener) => listener(event));
return {
listen: (fn) => {
listeners.push(fn);
return () => {
listeners = listeners.filter((l) => l !== fn);
};
},
add: (item) => {
const newItem = { ...item, id: createId() };
data = [...data, newItem];
dispatch({ type: 'add', doc: newItem });
},
edit: (id) => {
data = data.map((d) =>
d.id === id ? { ...d, count: d.count + 1 } : d
);
dispatch({
type: 'edit',
doc: data.find((d) => d.id === id),
});
},
};
})();
const Counter = React.memo(function Counter({ up, item }) {
return (
<button onClick={() => up(item.id)}>
{item.count}
</button>
);
});
function App() {
const [docList, setDocList] = React.useState([]);
React.useEffect(
() =>
firebase.listen(({ type, doc }) => {
if (type === 'add') {
setDocList((current) => [...current, doc]);
}
if (type === 'edit') {
setDocList((current) =>
current.map((item) =>
item.id === doc.id ? doc : item
)
);
}
}),
[]
);
const up = React.useCallback(
(id) => firebase.edit(id),
[]
);
return (
<div>
<button onClick={() => firebase.add({ count: 0 })}>
add
</button>
<div>
{docList.map((doc) => (
<Counter key={doc.id} up={up} item={doc} />
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can do setDocList(docList.map... but that makes docList a dependency of the effect: useEffect(function,[docList]) and the effect will run every time docList changes so you need to remove the listener and idd it every time.
In your code you did not add the dependency so docList was a stale closure. But the easiest way would be to do what I suggested and use callback for setDocList: setDocList(current=>current.map... so docList is not a dependency of the effect.
The comment:
I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement
Is simply not true, when you pass a callback to state setter the current state is passed to that callback.
Based on #BrettEast suggestion;
I know this isn't what you want to hear, but I would probably suggest using useReducer reactjs.org/docs/hooks-reference.html#usereducer, rather than useState for tracking an array of objects. It can make updating easier to track. As for your bug, I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement.
I use useReducer instead of useState and here is the working code:
import React, {useReducer, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const initialState = [];
/**
* reducer declaration for useReducer
* #param {[*]} state the current use reducer state
* #param {{payload:*,type:'add'|'modify'|'remove'}} action defines the function to be performed and the data needed to execute such function in order to modify the state variable
*/
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return [action.payload, ...state]
case 'modify':
const modIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`modified data found in idx: ${idx}, id: ${doc.id}`);
return true;
}
return false;
})
let newModState = state;
newModState.splice(modIdx,1,action.payload);
return [...newModState]
case 'remove':
const rmIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`data removed from idx: ${idx}, id: ${doc.id}, fullData: `,doc);
return true;
}
return false;
})
let newRmState = state;
newRmState.splice(rmIdx,1);
return [...newRmState]
default:
return [...state]
}
}
const DocList = ({firebase}) => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
dispatch({type:'add', payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'modified') {
dispatch({type:'modify',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'removed'){
dispatch({type:'remove',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
state.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
also according to #HMR, using the setState callback function:
here is the updated code which also worked if you're to use useState().
import React, { useState, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const DocList = ({firebase}) => {
const [docList, setDocList ] = useState([]);
const classes = useStyles();
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(current => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...current]);
console.log('document added: ', docSnap.doc.data());
}
if (docSnap.type === 'modified') {
setDocList(current => current.map(item => item.id === docSnap.doc.id ? {
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()} : item )
)
}
if (docSnap.type === 'removed'){
setDocList(current => {
const rmIdx = current.findIndex((doc, idx) => {
if (doc.id === docSnap.doc.id) {
return true;
}
return false;
})
let newRmState = current;
newRmState.splice(rmIdx, 1);
return [...newRmState]
})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
docList.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
Thanks hope this help whoever is experiencing similar problem.

useState not updating the state

I'm trying to filter users with input search inside the function searchByName.
I manage to get the right result in copyUsersvariable but unfortunately it does not reflect the change inside the state.
Forgot to mention, using bare React-App with hooks and typescript.
For example, i write 'p' in the input and recieve the right filtered array in copyUsers variable but then i push it into the state it does not update.
Attaching screenshot for understanding the situation better:
what i have tried instead setFilteredUsers(copyUsers):
setFilteredUsers(() => [...filteredUsers, copyUsers]);
setFilteredUsers(() => copyUsers);
main component:
const { value } = useSelector(({ test }: any) => test);
const [users, setUsers] = useState<Users>([]);
const [filteredUsers, setFilteredUsers] = useState<Users>([]);
const [searchNameValue, setSearchNameValue] = useState<string>("");
const [selectedUser, setSelectedUser] = useState<User>();
const [searchOrderBy, setSearchOrderBy] = useState<string>("");
const dispatch = useDispatch();
useEffect(() => {
const get = async () => {
const response = await ApiTest.testGet();
setUsers(response);
setSearchOrderBy("desc");
};
get();
}, []);
useEffect(() => {
searchByName();
setNewOrder();
}, [searchOrderBy]);
useEffect(() => {
console.log('search value changed!', searchNameValue);
searchByName();
setNewOrder()
}, [searchNameValue]);
const setNewOrder = () => {
if (users.length) {
let copyUsers = JSON.parse(
JSON.stringify(filteredUsers.length ? filteredUsers : users)
);
switch (searchOrderBy) {
case "desc":
copyUsers.sort((a: any, b: any) => {
if (a.id > b.id) {
return -1;
}
if (b.id > a.id) {
return 1;
}
return 0;
});
break;
case "asc":
copyUsers.sort((a: any, b: any) => {
if (b.id > a.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
return 0;
});
break;
default:
break;
}
filteredUsers.length ? setFilteredUsers(copyUsers) : setUsers(copyUsers);
}
};
const searchByName = () => {
if (searchNameValue) {
let copyUsers = JSON.parse(JSON.stringify(users));
copyUsers = copyUsers.filter((user: User) => {
return user.name
.toLocaleLowerCase()
.includes(searchNameValue.toLocaleLowerCase());
});
console.log("copyUsers =", copyUsers);
setFilteredUsers(copyUsers);
console.log("filteredUsers =", filteredUsers);
}
};
const UserCards =
!!users.length &&
(searchNameValue ? filteredUsers : users).map(user => {
return (
<UserCard
selectedUser={selectedUser}
setSelectedUser={(user: User) => setSelectedUser(user)}
user={user}
/>
);
});
return (
<div>
<FilterBar
searchOrderBy={searchOrderBy}
searchSetOrderBy={(value: string) => setSearchOrderBy(value)}
setSearchNameValue={(value: string) => setSearchNameValue(value)}
searchNameValue={searchNameValue}
/>
<div style={{ display: "flex", flexFlow: "wrap" }}>{UserCards}</div>
</div>
);

Resources