React Redux state cannot rerender component and state is UNDEFINED - reactjs

There is some issues with state in Redux. I want to change color of a component with state update. But the state in TargetList.js has error: TypeError: Cannot read property 'includes' of undefined.
The Reducer file is:
import {
SENSOR_HI,
SENSOR_UNHI,
} from "redux/actionTypes";
const initialState = {
title: "",
text: "",
detailedList: "hide",
};
const Reducer = (state = initialState, action) => {
switch (action.type) {
case SENSOR_HI:
return {
...state,
sensorarray: action.data,
};
case SENSOR_UNHI:
return {
...state,
sensorarray: [],
};
case FILTER_TAGS_CHANGED:
return {
...state,
filterTags: action.data,
};
default:
return state;
}
};
export default Reducer;
And the TargetList.js is:
import "assets/scss/global.scss";
import moment from "jalali-moment";
import React, { useState, useEffect, useRef, memo } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { GiMovementSensor, GiRadarSweep } from "react-icons/gi";
import { MdDescription, MdSignalWifiOff,MdNetworkWifi } from "react-icons/md";
import { connect } from "react-redux";
import ToggleButton from "react-switch";
import socketConn from "sockets";
import "./Leaflet.scss";
import classes from "./TargetsList.module.scss";
import { targetTypes, targetsIconsHandlerImage } from "helpers/icons";
import { BackButton } from "assets/js/Icons";
import ReactDOMServer from "react-dom/server";
let TEMP_TARGETS = [];
let counter = 0;
let now = new Date();
const TargetsList = memo((props) => {
const [sensorFiltersSelected, setSensorFiltersSelected] = useState([]);
const [sensorFiltersSelectedIds, setSensorFiltersSelectedIds] = useState([]);
const [sensorSelected, setSensorSelected] = useState(null);
const [targetFiltersSelected, setTargetFiltersSelected] = useState([]);
const [hi, sethi] = useState(false);
let filterType = (sensors) => {
if (sensorFiltersSelected.length == 0) {
return sensors;
} else {
let ids = sensorFiltersSelected.map((s) => s["_id"]);
return sensors.filter((s) => ids.includes(s.sensorType["_id"]));
}
};
let filterTextSensors = (sensors) => {
return sensors.filter((s) => s.latinName.includes(searchInput));
};
let toggleFilter = (sensor) => {
let filters = sensorFiltersSelected;
let ids = filters.map((f) => f["_id"]);
if (filters.length && ids.includes(sensor["_id"])) {
filters = filters.filter((f) => f["_id"] != sensor["_id"]);
} else {
filters.push(sensor);
}
props.dispatch({
type: "FILTER_TAGS_CHANGED",
data: [...filters],
});
setSensorFiltersSelected(filters);
setSensorFiltersSelectedIds(filters.map(s=>s.latinName))
props.dispatch({
type: "SENSOR_HI",
data: [filters.map(s=>s.latinName)],
});
};
return (
<div>
<div
className="container"
style={{
padding: 0,
}}
>
<div className="rowx my-2 flex-wrap">
{props.sensors && props.sensors.length
? filterTextSensors(props.sensors).map(
(s, sensorTypeIndex) => (
<div
key={s._id}
/////////////////////////////////////// Here is Error ///////////////////////////////////////////////
className={`cursor-pointer badge badge-pill m-1 ${props.sensorarray.includes(s.latinName) ? `badge-success` : `badge-light`}`}
onClick={() => {
showLogsSensor(s)
toggleFilter(s);
}}
>
{s.persianName}
</div>
)
)
: ""}
</div>
});
const mapStateToProps = (state) => {
return {
sensorarray: state.sensorarray,
};
};
export default connect(mapStateToProps)(TargetsList);
And the Drawer.js is change state that should color change in TargetList.js.
Drawer.js:
import React from "react";
import clsx from "clsx";
import {makeStyles} from "#material-ui/core/styles";
import Drawer from "#material-ui/core/Drawer";
import {connect} from "react-redux";
import "assets/scss/global.scss";
import "./style.scss";
import socketConn from "sockets";
import moment from "jalali-moment";
import {MdSave} from "react-icons/md";
import axios from "config.js";
const useStyles = makeStyles({
fullList: {
width: "auto",
},
});
function BottomDrawer(props) {
const classes = useStyles();
const [tab, setTab] = React.useState("sensors");
const [state, setState] = React.useState({bottom: false});
const [type, setType] = React.useState(null);
const [info, setInfo] = React.useState(null);
const [logs, setLogs] = React.useState([]);
const toggleDrawer = (anchor, open) => {
setState({
...state,
[anchor]: open,
});
// stop receiving target logs
targetsLogAPIHandler(false);
};
const sensorunhi = () => {
props.dispatch({
type: "SENSOR_UNHI",
});
};
let setLogSocket = (type, info) => {
setLogs([]);
if (type === "target") {
targetsLogAPIHandler(true, info);
}
if (type === "sensor") {
sensorsLogAPIHandler(info);
}
};
// handler for API request to NodeJS to get logs of specific target (! ! ! requestMode => {true: when you want to START coming logs | false: when you want to STOP coming logs})
const targetsLogAPIHandler = async (requestMode, info) => {
try {
const userId = localStorage.getItem('user_id');
socketConn().removeAllListeners("show_specific_target_log");
const {data} = await axios.post(`/targets/log/${userId}`, {
requestMode,
unique_id: info ? info.unique_id : undefined
});
socketConn().on("show_specific_target_log", targetLogs => {
setLogs((prev) => {
return [targetLogs, ...prev].filter((s, index) => index < 500);
});
});
} catch (err) {
console.log(err);
}
};
// handler for API request to NodeJS to get logs of specific sensor
const sensorsLogAPIHandler = async info => {
try {
socketConn().removeAllListeners("sensor_moment_log");
let userId = localStorage.getItem("user_id");
const logs = await axios.post(`sensors/sensors/logs`, {topic: info.latinName, userId});
socketConn().on("sensor_moment_log", (data) => {
let enc = new TextDecoder("utf-8");
let decoded = enc.decode(data.result.content);
decoded = JSON.parse(decoded);
setLogs((prev) => {
return [decoded, ...prev].filter((s, index) => index < 500);
});
});
socketConn().on("sensor_moment_log_error", (data) => {
console.log(data);
});
} catch (err) {
console.log(err);
}
};
React.useEffect(() => {
setTab((prev) => {
if (prev != props.selectedTab) {
socketConn().removeAllListeners("sensor_moment_log");
socketConn().removeAllListeners("show_specific_target_log");
setLogs([]);
return props.selectedTab;
}
return prev;
});
}, [props.selectedTab]);
React.useEffect(() => {
if (props.showLogs && state["bottom"] == false) {
toggleDrawer("bottom", true);
}
}, [props.showLogs]);
React.useEffect(() => {
if (props.logInfo && props.logType) {
if (info != null) {
if (
type != props.logType ||
info.Identification + info.sensorName !=
props.logInfo.Identification + props.logInfo.sensorName
) {
setType(props.logType);
setInfo(props.logInfo);
setLogSocket(props.logType, props.logInfo);
}
} else {
setType(props.logType);
setInfo(props.logInfo);
setLogSocket(props.logType, props.logInfo);
}
}
}, [props.logInfo, props.logType]);
const list = anchor => (
<div className={clsx(classes.list, {[classes.fullList]: anchor === "bottom",})} role="presentation">
<div className="log-container p-4">
<React.Fragment>
<div className="rowx log-headers mb-3">
<div className="colx log-header font-weight-bold">نام سنسور</div>
<div className="colx log-header font-weight-bold">نوع هدف</div>
<div className="colx log-header font-weight-bold">طول</div>
<div className="colx log-header font-weight-bold">عرض</div>
<div className="colx log-header font-weight-bold">ارتفاع</div>
</div>
<div className="log-elements">
{logs &&
logs.length &&
logs.map((log, index) => (
<div className="rowx log-element mb-2" key={index}>
<div className="colx log-text">{log && log[0] && log[0].sensorName}</div>
<div className="colx log-text">{log && log[0] && log[0].sensorType}</div>
<div className="colx log-text">{log && log[0] && log[0].longitude}</div>
<div className="colx log-text">{log && log[0] && log[0].latitude}</div>
<div className="colx log-text">{log && log[0] && log[0].altitude}</div>
</div>
))}
</div>
</React.Fragment>
)}
</div>
</div>
);
return (
<div>
<React.Fragment>
<Drawer
classes={{root: "drawer-root"}}
ModalProps={{hideBackdrop: true}}
anchor="bottom"
open={state["bottom"]}
onClose={() => {
toggleDrawer("bottom", false);
sensorunhi(); /////////////////// CHANGE REDUX STATE
}}
>
{list("bottom")}
</Drawer>
</React.Fragment>
</div>
);
}
const mapStateToProps = (state) => {
return {
sensorarray: state.sensorarray,
};
};
export default connect(mapStateToProps)(BottomDrawer);
In other word, I want to change the color component TargetList.js by change the state in Drawer.js.

Related

MobX doesn't rerender my FC for some reason, I can't explain why

So there is a storage containing 2 lists, one of the lists is fetching from server (using axios).
Both the lists renders by FC - PlayerList.
RondomizerPage contains these 2 lists.
So when I F5 my page on "localhost:3000/randomizer" nothing appears to be in my fetched list(fetched confirmed). But when I click anything(link to "randomizer" for example or any button) rerender goes in, and my list appears.
The 'store':
import { makeAutoObservable } from "mobx";
import { IPlayerInfo } from "../types/player";
import axios from "../axios";
import IMove from "./../types/movement";
class Player {
playerList: IPlayerInfo[] = [];
randomPlayerList: IPlayerInfo[] = [];
isAuth: boolean = false;
constructor() {
makeAutoObservable(this);
}
addPlayer(playerdata: IPlayerInfo) {
this.playerList.push(playerdata);
}
move(from: IMove, to: IMove) {
if (from.list === to.list) {
const list = from.list;
if (list === "randomList") {
const [temp] = this.randomPlayerList.splice(from.index, 1);
this.randomPlayerList.splice(to.index, 0, temp);
}
if (list === "playerList") {
const [temp] = this.playerList.splice(from.index, 1);
this.playerList.splice(to.index, 0, temp);
}
} else {
if (from.list === "playerList") {
const [temp] = this.playerList.splice(from.index, 1);
this.randomPlayerList.splice(to.index, 0, temp);
}
if (from.list === "randomList") {
const [temp] = this.randomPlayerList.splice(from.index, 1);
this.playerList.splice(to.index, 0, temp);
}
}
}
fetchPlayers = async () => {
try {
const response = await axios.get("/players");
this.playerList = response.data;
return
} catch (error) {
console.log(error);
}
};
randomPlayers = async () => {
if (
this.randomPlayerList.length % 2 === 0 &&
this.randomPlayerList.length > 1
) {
const response = await axios.post("/random", {
amount: this.randomPlayerList.length,
});
const data = response.data.result.random.data[0];
let playerList = [];
for (let index = 0; index < data.length; index++) {
playerList.push(this.randomPlayerList[data[index] - 1]);
}
return playerList;
}
return false;
};
}
const playerStore = new Player()
export default playerStore;
And I have a 'Randomizer page':
import React, { useEffect, useState } from "react";
import PlayerList from "../../components/Lists/PlayerList";
import players from "../../store/players";
import styles from "./RandomizerPage.module.scss";
import { useNavigate } from "react-router";
import axios from "../../axios";
import { DragDropContext, Droppable, DropResult } from "react-beautiful-dnd";
import IMove from "./../../types/movement";
import PlayerCreation from "../../components/Modals/PlayerCreationModal/PlayerCreation";
import MyButton from "../../components/UI/MyButton/MyButton";
import RandomedPlayersModal from "../../components/Modals/RandomedPlayersModal/RandomedPlayersModal";
import { observer } from "mobx-react-lite";
const RandomizerPage = observer(() => {
const [playerCreationModalisOn, setPlayerCreationModalisOn] =
useState(false);
const [randomedPlayerModalIsOn, setRandomedPlayerModalIsOn] =
useState(false);
const nav = useNavigate();
const onDragEndHandle = (result: DropResult) => {
if (!result.destination) return;
const from: IMove = {
index: result.source.index,
list: result.source.droppableId,
};
const to: IMove = {
index: result.destination.index,
list: result.destination.droppableId,
};
players.move(from, to);
};
const playerCreationHandler = () => {
setPlayerCreationModalisOn(true);
};
const playerCreationAbortion = () => {
setPlayerCreationModalisOn(false);
};
const randomPlayersHandler = () => {
const response = players.randomPlayers();
console.log(response);
setRandomedPlayerModalIsOn(true);
};
const randomPlayerAbortion = () => {
setRandomedPlayerModalIsOn(false);
};
useEffect(() => {
const user = JSON.parse(localStorage.getItem("user") || "null");
if (user) {
axios.defaults.headers["authorization"] = user.token;
players.fetchPlayers();
} else nav("/login");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
{playerCreationModalisOn && (
<PlayerCreation
abort={playerCreationAbortion}
title={"Создание персонажа"}
/>
)}
{randomedPlayerModalIsOn && (
<RandomedPlayersModal
abort={randomPlayerAbortion}
title={"random.org выбрал команды"}
/>
)}
<DragDropContext onDragEnd={onDragEndHandle}>
<div className={styles.wrapper}>
<div className={styles.column}>
<div>
<h4>Переместите сюда игроков</h4>
<Droppable droppableId="randomList">
{(provided) => (
<div
className={styles.list}
{...provided.droppableProps}
ref={provided.innerRef}
>
<PlayerList list={players.randomPlayerList} />
{provided.placeholder}
</div>
)}
</Droppable>
</div>
<MyButton click={randomPlayersHandler}>
Сгенерировать случайные команды
</MyButton>
</div>
<div className={styles.column}>
<div>
<h4>Игроки</h4>
<Droppable droppableId="playerList">
{(provided) => (
<div
className={styles.list}
{...provided.droppableProps}
ref={provided.innerRef}
>
<PlayerList list={players.playerList} />
{provided.placeholder}
</div>
)}
</Droppable>
</div>
<MyButton click={playerCreationHandler}>
Добавить игрока
</MyButton>
</div>
</div>
</DragDropContext>
</>
);
});
export default RandomizerPage;
And my PlayerList looks like that, btw:
import React from "react";
import { observer } from "mobx-react-lite";
import { Draggable } from "react-beautiful-dnd";
import PlayerCard from "../PlayerCard/PlayerCard";
import { IPlayerInfo } from "../../types/player";
interface PlayerListProps {
list: IPlayerInfo[];
}
const PlayerList = observer(({ list }: PlayerListProps) => {
console.log('rerender', list);
return (
<>
{list.map((player: IPlayerInfo, index: number) => (
<Draggable
key={player.steamId}
draggableId={player.steamId}
index={index}
>
{(provided) => (
<PlayerCard
playerinfo={player}
{...provided.draggableProps}
{...provided.dragHandleProps}
innerRef={provided.innerRef}
/>
)}
</Draggable>
))}
</>
);
});
export default PlayerList;
So, I found the crutch, if I add in RandomizerPage and change list={playerList}:
const playerList = players.playerList

Still re-rendering using React.memo when the prop remain unchanged

I am creating a dropzone uploader for video-uploading (using react-dropzone-uploader package),
and also get its thumbnail with package ThumbnailExtractor.
I used React.Memo(base64) to ensure if the thumbnail's base64 remain unchanged, React will not re-render.
However, when I click submit button, the thumbnail keep re-rendering even the base64 remain unchanged.
Any clue?
import React, { useEffect, useState } from "react";
import axios from "axios";
import produce from "immer";
import Dropzone from "react-dropzone-uploader";
import ThumbnailContainer from "./ThumbnailContainer";
require("./dropzone.scss");
const DropZoneUploader = ({ api, setFiles }) => {
const [clonedFiles, setClonedFiles] = useState([]);
const UploadFileWithProgress = async (f, formData) => {
try {
const options = {
onUploadProgress: (progressEvent) => {
const { loaded, total } = progressEvent;
let percent = Math.floor((loaded * 100) / total);
console.log(`${loaded}kb of ${total}kb | ${percent}%`);
if (percent < 100) {
setClonedFiles(
produce((draft) => {
const newItem = draft.find(
(item) => item.meta.id === f.meta.id
);
newItem.percent = percent;
})
);
} else {
setClonedFiles(
produce((draft) => {
const newItem = draft.find(
(item) => item.meta.id === f.meta.id
);
newItem.percent = 99;
})
);
}
}
};
await axios
.post("https://reqres.in/api/users", formData, options)
.then(async (res) => {
console.log(res);
f.meta.percent = 100;
setClonedFiles(
produce((draft) => {
const newItem = draft.find((item) => item.meta.id === f.meta.id);
newItem.true_status = "uploaded";
})
);
setFiles(
produce((draft) => {
draft.unshift(res.data.data);
})
);
f.remove();
});
} catch (err) {
console.log(err);
return null;
}
};
// called every time a file's `status` changes
const handleChangeStatus = ({ meta, file }, status) => {
if (status === "done") {
setClonedFiles(
produce((draft) => {
draft.push({
file,
meta,
true_status: "ready"
});
})
);
}
if (status === "removed") {
setClonedFiles(
produce((draft) => {
const newItemIndex = draft.findIndex(
(item) => item.meta.id === meta.id && item.true_status === "ready"
);
if (newItemIndex !== -1) {
draft.splice(newItemIndex, 1);
}
})
);
}
};
// receives array of files that are done uploading when submit button is clicked
const handleSubmit = (files, allFiles) => {
files.map(async (f) => {
const formData = new FormData();
formData.append("title", `${f.meta.name}`);
formData.append("type", f.meta.type);
formData.append("file_purpose", "private");
formData.append("file", f.file);
const reee = await UploadFileWithProgress(f, formData);
if (reee) {
console.log("pushed");
}
});
};
const PreviewOutput = ({ files }) => {
const iconByFn = {
cancel: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
},
remove: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
},
restart: {
backgroundImage: `url(https://upload.wikimedia.org/wikipedia/commons/d/dc/Cancel_icon.svg)`
}
};
return (
<div className="dropzone__imgs-wrapper">
{clonedFiles?.map((f) => (
<div key={f.meta.id}>
<ThumbnailContainer f={f} files={files} />
</div>
))}
</div>
);
};
const MyCustomPreview = ({
input,
previews,
submitButton,
dropzoneProps,
files,
}) => (
<div className="dropzone">
<div {...dropzoneProps}>
<PreviewOutput files={files} />
{input}
</div>
{submitButton}
</div>
);
return (
<Dropzone
onChangeStatus={handleChangeStatus}
onSubmit={handleSubmit}
autoUpload={true}
accept="video/*"
LayoutComponent={(props) => <MyCustomPreview {...props} />}
/>
);
};
export default DropZoneUploader;
ThumbnailContainer.jsx
import React, { useEffect, useState } from "react";
import ThumbnailExtractor from "react-thumbnail-extractor";
import ThumbnailImageSrc from "./ThumbnailImageSrc";
const ThumbnailContainer = ({ f, files }) => {
const [vFile, setVFile] = useState();
const [vImage, setVImage] = useState();
const [isCaptured, setIsCaptured] = useState(false);
const fileFunction = files.find((a) => a.meta.id === f.meta.id);
const { name, previewUrl } = f.meta;
const { true_status, percent } = f;
useEffect(() => {
setVFile(f.file);
console.log("fFile render");
}, [f.file]);
return (
<div key={f.meta.id} className="dropzone__img">
{!previewUrl && (
<>
<ThumbnailExtractor
maxWidth={600}
videoFile={vFile}
count={1}
onCapture={(image) => {
if (!isCaptured) {
console.log("capture Render");
setVImage(image[0]);
setIsCaptured(true);
}
}}
/>
<span className="dzu-previewFileName">{name}</span>
</>
)}
<div className="dzu-previewStatusContainer">
{fileFunction &&
fileFunction.meta.percent !== 100 &&
f.true_status !== "uploaded" ? (
<progress max={100} value={percent} />
) : (
""
)}
{true_status === "ready" && (
<button
className="dropzone__img-delete"
type="button"
onClick={fileFunction?.remove}
>
<i className="las la-times"></i>
</button>
)}
{vImage && <ThumbnailImageSrc src={vImage} />}
</div>
</div>
);
};
export default ThumbnailContainer;
ThumbnailImageSrc.jsx (React.memo)
import React from "react";
export const ThumbnailImageSrc = React.memo(function ThumbnailImageSrc({
src
}) {
return (
<div
key={src}
className="dropzone_thumbnail-preview"
style={{ backgroundImage: `url(${src}` }}
></div>
);
});
export default ThumbnailImageSrc;
Here is the codesandbox sample.

How can i test add filters button when clicked it opens search filters popup but before the button appears there is loading spinner?

add filter button is appeared when loading spinner is completed(means there is dispatch trigger action called loadListings which set loading to true).
I'm using react testing library, how can i achieve this by using mockedAxios? or any idea.
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
Search.js
import React, {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {useNavigate} from '#reach/router';
import Picture from '#/components/picture';
import Button from '#/components/button';
import Pagination from '#/components/pagination';
import Tag from '#/components/tag';
import Spinner from '#/components/basic-spinner';
import Footer from '#/components/footer/footer';
import {
getAppliedSelectFilters,
getListings,
getListingsTotal,
listingsLoading,
} from '#/selectors/search';
import {showPopup} from '#/redux/modules/app/actions';
import {
saveDraftFilters,
clearDraftFilters,
draftSelectFilter,
clearAppliedFilter,
clearAllAppliedFilters,
loadListings,
} from '#/redux/modules/search/actions';
import {getSelectFilters, getSelectFiltersGroups} from '#/selectors/search';
import SearchFilterPopup from '#/components/search-filter-popup';
const Search = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const appliedFilters = useSelector(getAppliedSelectFilters);
const hits = useSelector(getListings);
const hitsCount = useSelector(getListingsTotal);
const loading = useSelector(listingsLoading);
const hitsPerPage = 10;
useEffect(() => {
dispatch(loadListings());
}, [dispatch]);
const showFilters = () => {
dispatch(
showPopup({
content: (
<SearchFilterPopup
saveDraftFilters={saveDraftFilters}
clearDraftFilters={clearDraftFilters}
draftSelectFilter={draftSelectFilter}
loadListings={loadListings}
selectFiltersSelector={getSelectFilters}
selectFiltersGroupsSelector={getSelectFiltersGroups}
/>
),
showClose: false,
}),
);
};
const handleTagClicked = item => {
dispatch(clearAppliedFilter(item));
dispatch(loadListings());
};
// eslint-disable-next-line #typescript-eslint/no-unused-vars
const handleClearAllFilters = () => {
dispatch(clearAllAppliedFilters());
dispatch(loadListings());
};
const handleBusinessSelected = ({identity, urlName}) => () => {
navigate(`/listings/${urlName}/${identity}`);
};
const handlePaginationPageClick = data => {
const indexOfLastHit = (data.selected + 1) * hitsPerPage;
const pages = indexOfLastHit - hitsPerPage;
dispatch(loadListings(pages));
};
return (
<section className='np-flex np-w-full np-filter-container np-overflow-x-hidden np-max-w-1300px np-flex-col np-justify-between np-mx-auto np-min-h-screen'>
<div className='np-flex np-pd np-flex-col np-justify-center'>
{!loading ? (
<div className='np-w-full'>
<div className='np-w-full'>
<h3 className='np-font-primary np-result-places np-mb-3 np-pt-8 np-truncate'>
<span>{hitsCount === 0 ? hitsCount : `${hitsCount}+`}</span>{' '}
{`Place${hitsCount !== 1 ? 's' : ''}`} in Dar es Salaam
</h3>
<div className='np-mb-3 np-flex'>
<Button
variant='link'
className='np-font-semibold np-uppercase'
onClick={showFilters}
>
<span>Add Filters</span>
</Button>
</div>
<div className='np-overflow-x-auto np-flex np-w-full'>
{appliedFilters.map(item => (
<Tag
key={item.name}
item={item}
labelKey='label'
toBe='removed'
onClick={handleTagClicked}
className='np-Tag-width'
/>
))}
{appliedFilters.length ? (
<Button variant='link' onClick={handleClearAllFilters}>
Clear all filters
</Button>
) : null}
</div>
</div>
</div>
) : null}
<div className='np-flex np-flex-wrap np-justify-center'>
{loading ? (
<div className='np-pt-32 np-h-80vh np-w-full np-flex np-justify-center'>
<Spinner size='large' label='loading' color='primary' />
</div>
) : hitsCount > 0 ? (
hits.map(hit => (
<div
key={hit.id}
className='np-search-card'
onClick={handleBusinessSelected(hit)}
>
<Picture
height='2/3'
src={`${
process.env.IMAGE_SERVICE_URL
}/cover:entropy/340x226/${hit.photos.length > 0 &&
hit.photos[hit.photos.length - 1].name}`}
className='np-overflow-hidden np-rounded-sm np-cursor-pointer'
/>
<section className='np-py-2 np-leading-normal'>
<h4 className='np-truncate'>{hit.category}</h4>
<h3 className='np-font-primary np-font-medium np-cursor-pointer np-truncate np-capitalize'>
{hit.name}{' '}
</h3>
<h4 className='np-text np-text-gray np-truncate'>
<span className='icon location'></span> {hit.location}
</h4>
</section>
</div>
))
) : null}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
actions.js
export const clearDraftFilters = () => ({
type: types.CLEAR_DRAFT_FILTERS,
});
export const loadListings = payload => ({
type: types.LOAD_LISTINGS,
payload,
});
export const loadListingStart = () => ({
type: types.LOAD_LISTINGS_START,
});
export const loadListingSuccess = payload => ({
type: types.LOAD_LISTINGS_SUCCESS,
payload,
});
export const loadListingError = error => ({
type: types.LOAD_LISTINGS_ERROR,
payload: error,
error: true,
});
epics.js
export const loadListingsEpic = (action$, state$, {API}) =>
action$.pipe(
filter(action => action.type === types.LOAD_LISTINGS),
switchMap(action => {
const features = getAppliedPlaceFeatureFilters(state$.value).join(',');
const category = getAppliedPlaceCategoryFilters(state$.value).join(',');
const q = get(state$.value, 'search.q', '');
const page = action.payload ? action.payload : null;
const query = omitEmpty({q, category, features, page, limit: 12});
return defer(() => API.getListings(query)).pipe(
switchMap(response => {
const [error, result] = response;
if (error) {
return of(actions.loadListingError(error));
}
return of(actions.loadListingSuccess(result));
}),
startWith(actions.loadListingStart()),
);
}),
);
reducers.js
const initialState = {
selectFilters: [],
suggestions: {docs: [], total: 0, loading: false},
};
export default (state = initialState, action) => {
switch (action.type) {
case types.LOAD_LISTINGS_START:
return {
...state,
listings: {loading: true},
};
case types.LOAD_LISTINGS_SUCCESS:
return {
...state,
listings: {loading: false, ...action.payload},
};
case types.LOAD_LISTINGS_ERROR:
return {
...state,
listings: {loading: false, error: action.payload},
};
default:
return state;
}
};
Search.test.js
import React from 'react';
import {render, screen, fireEvent} from '#/utils/testUtils';
import {waitFor} from '#testing-library/react';
import mockedAxios from 'axios';
import Search from './Search';
import { async } from 'rxjs/internal/scheduler/async';
jest.mock('axios');
describe('Search', () => {
afterEach(() => {
jest.resetAllMocks();
});
test('should render search result filters popup when add filters button is clicked', async () =>{
render(<Search />);
await waitFor(() => fireEvent.click(screen.getByRole('button', {name: 'Add Filters'}))
)
})
});

Component not rerendering after axios Get (React)

I'm trying to render List of items of my DB using React.Context.
All my request work pretty well.
when i console log my states first I get an empty array and then array with the data that I need but my component is not updating. I have to go to another page an then go back to this page to get the data. I don't really understand why... here are my files..
ArticlesContext.js :
import React, { useState, createContext, useEffect } from 'react';
import axios from 'axios'
export const ArticlesContext = createContext();
export function ArticlesProvider(props) {
const [articles, setArticles] = useState([]);
const [user, setUser] =useState(0)
async function getArticles () {
await axios.get(`/api/publicItem`)
.then(res => {
setArticles(res.data);
})
}
useEffect( () => {
getArticles()
}, [user])
console.log(articles);
return (
<ArticlesContext.Provider value={[articles, setArticles]}>
{props.children}
</ArticlesContext.Provider>
);
}
Inventaire.js :
import React, { useContext, useEffect, useState } from 'react';
import './Inventaire.css';
import { ArticlesContext } from '../../../context/ArticlesContext';
import DeleteAlert from './Delete/Delete';
import Modify from './Modify/Modify';
import Filter from './Filter/Filter';
import axios from 'axios'
import Crud from '../../Elements/Articles/Crud/Crud';
import List from './List/List';
export default function Inventaire() {
const [articles, setArticles] = useContext(ArticlesContext);
const [filter, setFilter] = useState(articles)
console.log(articles);
//list for Inputs
const cat = articles.map(a => a.category.toLowerCase());
const categoryFilter = ([...new Set(cat)]);
const gender = articles.map(a => a.gender.toLowerCase());
const genderFilter = ([...new Set(gender)]);
//Event Listenner
//Uncheck All checkboxes
function UncheckAll() {
const el = document.querySelectorAll("input.checkboxFilter");
console.log(el);
for (var i = 0; i < el.length; i++) {
var check = el[i];
if (!check.disabled) {
check.checked = false;
}
}
}
//SearchBar
const searchChange = (e) => {
e.preventDefault();
const stuff = articles.filter((i) => {
return i.name.toLowerCase().match(e.target.value.toLowerCase())
})
setFilter(stuff)
UncheckAll(true)
}
const Types = (e) => {
if (e.target.checked === true) {
const stuff = filter.filter((i) => {
return i.category.toLowerCase().match(e.target.value.toLowerCase())
})
setFilter(stuff)
console.log(articles);
} else if (e.target.checked === false) {
setFilter(articles)
}
}
const Gender = (e) => {
if (e.target.checked === true) {
const stuff = filter.filter((i) => {
console.log(i.category, e.target.value);
return i.gender.toLowerCase().match(e.target.value.toLowerCase())
})
setFilter(stuff)
} else if (e.target.checked === false) {
setFilter(articles)
}
}
return (
<div className="inventaireContainer">
<input type="text" placeholder="Recherche un Article" onChange={searchChange} />
<div className="inventaireMenu">
<Crud />
<Filter
filter={Types}
categorys={categoryFilter}
genre={genderFilter}
target={Gender}
/>
</div>
<List filter={filter} articles={articles}/>
</div>
)
}
List.js :
import React from 'react';
import DeleteAlert from '../Delete/Delete';
import Modify from '../Modify/Modify';
export default function List({ filter, articles }) {
return (
<div>
{filter.map((details, i) => {
return (
<div className="inventaireBlock" >
<div className="inventaireGrid">
<div className="inventaireItemImg">
<img src={details.image} alt="ItemImg" />
</div>
<h2>{details.name}</h2>
<h3>{details.category}</h3>
<h3>{details.gender}</h3>
<div>
<p>S :{details.sizes[0].s}</p>
<p>M :{details.sizes[0].m}</p>
<p>L :{details.sizes[0].l}</p>
<p>XL :{details.sizes[0].xl}</p>
</div>
<h2> Prix: {details.price}</h2>
<div className="modify">
<Modify details={details._id} />
</div>
<div className="delete" >
<DeleteAlert details={details._id} articles={articles} />
</div>
</div>
</div>
)
})}
</div>
)
}
Thanks for your time

Why does my map-function break Swiperjs component in React?

I'm trying to build a Movie Search web app with React. I query themoviedb.com for movie information and display the poster/movie title to the user.
I'm using Swiperjs to display the movie posters and titles to the users horizontally. Unfortunately, when I map() the movie information into a SwiperSlide component, the slider won't slide. It seems to stutter and refuse to move from the first movie poster. Can anyone send me in the right direction as to why it won't work?
Weirdly enough, on random refreshes, random Genres will work completely fine until a reload, but only one Genre component will do this.
Genre Component:
import React from "react";
import { Swiper, SwiperSlide } from 'swiper/react';
import MovieCard from '../components/movieCard';
class Genre extends React.Component {
constructor(props) {
super(props);
this.state = {
genre: this.props.genre,
genreName: "",
movies: []
};
}
componentDidMount() {
const genreNum = this.state.genre;
const url = `apiURlRequestHere`;
try {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState({ movies: data.results }));
} catch (err) {
console.error(err);
}
this.getGenreName(genreNum);
}
getGenreName(genreNum) {
let title = "";
switch (genreNum) {
case "28":
title = "Action";
break;
case "12":
title = "Adventure";
break;
case "16":
title = "Animation";
break;
case "35":
title = "Comedy";
break;
case "80":
title = "Crime";
break;
case "99":
title = "Documentary";
break;
case "18":
title = "Drama";
break;
case "14":
title = "Fantasy";
break;
case "27":
title = "Horror";
break;
case "9648":
title = "Mystery";
break;
case "10749":
title = "Romance";
break;
case "878":
title = "Science Fiction";
break;
case "53":
title = "Thriller";
break;
default:
title = "";
break;
}
this.setState({ genreName: title });
}
render() {
const movies = this.state.movies;
let genreCat = this.state.genreName;
return (
<>
<h2 className="category-title">{genreCat}</h2>
<Swiper spaceBetween={0} slidesPerView={1}>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie} />
</SwiperSlide>
))}
</Swiper>
</>
);
}
}
export default Genre;
MovieCard Component:
import React from "react";
export default function MovieCard({movie}) {
return (
<div className="card">
<img
className="card--image"
src={`https://image.tmdb.org/t/p/w185_and_h278_bestv2/${movie.poster_path}`}
alt={movie.title + ' poster'}
/>
<div className="card--content">
<h3 className="card--title">{movie.title}</h3>
<p className="card--rating">{movie.vote_average * 10}%</p>
</div>
</div>
)
}
--- UPDATE ---
Here is the Swiper Component I am using from SwiperJs
import React, { useRef, useState, useEffect, forwardRef } from 'react';
import { getParams } from './get-params';
import { initSwiper } from './init-swiper';
import { needsScrollbar, needsNavigation, needsPagination, uniqueClasses } from './utils';
import { renderLoop, calcLoopedSlides } from './loop';
import { getChangedParams } from './get-changed-params';
import { getChildren } from './get-children';
import { updateSwiper } from './update-swiper';
import { renderVirtual, updateOnVirtualData } from './virtual';
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
const Swiper = forwardRef(
(
{
className,
tag: Tag = 'div',
wrapperTag: WrapperTag = 'div',
children,
onSwiper,
...rest
} = {},
externalElRef,
) => {
const [containerClasses, setContainerClasses] = useState('swiper-container');
const [virtualData, setVirtualData] = useState(null);
const [breakpointChanged, setBreakpointChanged] = useState(false);
const initializedRef = useRef(false);
const swiperElRef = useRef(null);
const swiperRef = useRef(null);
const oldPassedParamsRef = useRef(null);
const oldSlides = useRef(null);
const nextElRef = useRef(null);
const prevElRef = useRef(null);
const paginationElRef = useRef(null);
const scrollbarElRef = useRef(null);
const { params: swiperParams, passedParams, rest: restProps } = getParams(rest);
const { slides, slots } = getChildren(children);
const changedParams = getChangedParams(
passedParams,
oldPassedParamsRef.current,
slides,
oldSlides.current,
);
oldPassedParamsRef.current = passedParams;
oldSlides.current = slides;
const onBeforeBreakpoint = () => {
setBreakpointChanged(!breakpointChanged);
};
Object.assign(swiperParams.on, {
_containerClasses(swiper, classes) {
setContainerClasses(classes);
},
_swiper(swiper) {
swiper.loopCreate = () => {};
swiper.loopDestroy = () => {};
if (swiperParams.loop) {
swiper.loopedSlides = calcLoopedSlides(slides, swiperParams);
}
swiperRef.current = swiper;
if (swiper.virtual && swiper.params.virtual.enabled) {
swiper.virtual.slides = slides;
swiper.params.virtual.cache = false;
swiper.params.virtual.renderExternal = setVirtualData;
swiper.params.virtual.renderExternalUpdate = false;
}
},
});
if (swiperRef.current) {
swiperRef.current.on('_beforeBreakpoint', onBeforeBreakpoint);
}
useEffect(() => {
return () => {
if (swiperRef.current) swiperRef.current.off('_beforeBreakpoint', onBeforeBreakpoint);
};
});
// set initialized flag
useEffect(() => {
if (!initializedRef.current && swiperRef.current) {
swiperRef.current.emitSlidesClasses();
initializedRef.current = true;
}
});
// watch for params change
useIsomorphicLayoutEffect(() => {
if (changedParams.length && swiperRef.current && !swiperRef.current.destroyed) {
updateSwiper(swiperRef.current, slides, passedParams, changedParams);
}
});
// update on virtual update
useIsomorphicLayoutEffect(() => {
updateOnVirtualData(swiperRef.current);
}, [virtualData]);
// init swiper
useIsomorphicLayoutEffect(() => {
if (externalElRef) {
externalElRef.current = swiperElRef.current;
}
if (!swiperElRef.current) return;
initSwiper(
{
el: swiperElRef.current,
nextEl: nextElRef.current,
prevEl: prevElRef.current,
paginationEl: paginationElRef.current,
scrollbarEl: scrollbarElRef.current,
},
swiperParams,
);
if (onSwiper) onSwiper(swiperRef.current);
// eslint-disable-next-line
return () => {
if (swiperRef.current && !swiperRef.current.destroyed) {
swiperRef.current.destroy();
}
};
}, []);
// bypass swiper instance to slides
function renderSlides() {
if (swiperParams.virtual) {
return renderVirtual(swiperRef.current, slides, virtualData);
}
if (!swiperParams.loop || (swiperRef.current && swiperRef.current.destroyed)) {
return slides.map((child) => {
return React.cloneElement(child, { swiper: swiperRef.current });
});
}
return renderLoop(swiperRef.current, slides, swiperParams);
}
return (
<Tag
ref={swiperElRef}
className={uniqueClasses(`${containerClasses}${className ? ` ${className}` : ''}`)}
{...restProps}
>
{slots['container-start']}
{needsNavigation(swiperParams) && (
<>
<div ref={prevElRef} className="swiper-button-prev" />
<div ref={nextElRef} className="swiper-button-next" />
</>
)}
{needsScrollbar(swiperParams) && <div ref={scrollbarElRef} className="swiper-scrollbar" />}
{needsPagination(swiperParams) && (
<div ref={paginationElRef} className="swiper-pagination" />
)}
<WrapperTag className="swiper-wrapper">
{slots['wrapper-start']}
{renderSlides()}
{slots['wrapper-end']}
</WrapperTag>
{slots['container-end']}
</Tag>
);
},
);
Swiper.displayName = 'Swiper';
export { Swiper };
And here is the SwiperSlide Component:
import React, { useRef, useState, forwardRef } from 'react';
import { uniqueClasses } from './utils';
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
const SwiperSlide = forwardRef(
(
{ tag: Tag = 'div', children, className = '', swiper, zoom, virtualIndex, ...rest } = {},
externalRef,
) => {
const slideElRef = useRef(null);
const [slideClasses, setSlideClasses] = useState('swiper-slide');
function updateClasses(swiper, el, classNames) {
if (el === slideElRef.current) {
setSlideClasses(classNames);
}
}
useIsomorphicLayoutEffect(() => {
if (externalRef) {
externalRef.current = slideElRef.current;
}
if (!slideElRef.current || !swiper) return;
if (swiper.destroyed) {
if (slideClasses !== 'swiper-slide') {
setSlideClasses('swiper-slide');
}
return;
}
swiper.on('_slideClass', updateClasses);
// eslint-disable-next-line
return () => {
if (!swiper) return;
swiper.off('_slideClass', updateClasses);
};
});
let slideData;
if (typeof children === 'function') {
slideData = {
isActive:
slideClasses.indexOf('swiper-slide-active') >= 0 ||
slideClasses.indexOf('swiper-slide-duplicate-active') >= 0,
isVisible: slideClasses.indexOf('swiper-slide-visible') >= 0,
isDuplicate: slideClasses.indexOf('swiper-slide-duplicate') >= 0,
isPrev:
slideClasses.indexOf('swiper-slide-prev') >= 0 ||
slideClasses.indexOf('swiper-slide-duplicate-prev') >= 0,
isNext:
slideClasses.indexOf('swiper-slide-next') >= 0 ||
slideClasses.indexOf('swiper-slide-duplicate next') >= 0,
};
}
const renderChildren = () => {
return typeof children === 'function' ? children(slideData) : children;
};
return (
<Tag
ref={slideElRef}
className={uniqueClasses(`${slideClasses}${className ? ` ${className}` : ''}`)}
data-swiper-slide-index={virtualIndex}
{...rest}
>
{zoom ? (
<div
className="swiper-zoom-container"
data-swiper-zoom={typeof zoom === 'number' ? zoom : undefined}
>
{renderChildren()}
</div>
) : (
renderChildren()
)}
</Tag>
);
},
);
SwiperSlide.displayName = 'SwiperSlide';
export { SwiperSlide };
Also, I have a working example with the issue here... https://codesandbox.io/s/blue-mountain-0fjyc?file=/src/App.js
in this example, hit refresh on the browser and that is what I see.
--- Update ---
Still working on this project and I created a movie detail page that displays the cast with a Swiper(what a fool, I know). It works perfectly fine but if I replace the code in the GenreComponent code with the working code from the below MovieDetail component, it still will not work...
import React from "react";
import { Swiper, SwiperSlide } from 'swiper/react';
import MovieGenre from '../components/movieGenreBtn';
class MovieDetails extends React.Component {
constructor(props) {
super(props);
this.state = {
movie: [],
movieGenres: [],
credits: [],
director: [],
foundDirector: false
}
}
componentDidMount() {
const movieId = this.props.location.pathname.replace("/", "");
this.fetchMovie(movieId);
this.fetchCrew(movieId);
}
fetchMovie(movieId) {
const url = `https://api.themoviedb.org/3/movie/${movieId}?api_key=${process.env.REACT_APP_MOVIE_API_KEY}&language=en-US
`;
try {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState({ movie: data }));
} catch (err) {
console.error(err);
}
}
fetchCrew(movieId) {
const url = `https://api.themoviedb.org/3/movie/${movieId}/credits?api_key=${process.env.REACT_APP_MOVIE_API_KEY}`
try {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState({ credits: data }));
} catch (err) {
console.error(err);
}
}
getDirector() {
let director = [];
if (this.state.credits !== null){
const crew = this.state.credits.crew;
let i;
for(i=0; i < crew.length; i++) {
if (crew[i].job === 'Director') {
director = crew[i];
}
}
}
this.setState({ director: director, foundDirector: true });
}
getGenres() {
const movie = this.state.movie;
let genres = [];
let i;
for (i=0; i < movie.genres.length; i++) {
genres.push(movie.genres[i]);
}
this.setState({ movieGenres: genres });
}
render() {
const movie = this.state.movie;
const cast = this.state.credits.cast;
if (cast != null && !this.state.foundDirector) {
this.getDirector();
this.getGenres();
}
const currCast = this.state.credits.cast;
const movieGenres = this.state.movieGenres;
return (
<div className="movie-details-wrapper">
<div className="details-header">
<img
className="movie-backdrop"
src={`https://image.tmdb.org/t/p/w1000_and_h450_multi_faces/${movie.backdrop_path}`} />
<h3 className="details-title">
{movie.title}
</h3>
<h5 className="details-tagling">
{movie.tagline}
</h5>
</div>
<div className="details-content">
<div className="details-director-rating">
<p className="basic-details">
Director: {this.state.director.name}
</p>
<p className="details-rating">
{movie.vote_average * 10}%
</p>
</div>
<div className="details-genres">
{movieGenres.map(genre => (
<MovieGenre genre={genre.id} />
))}
</div>
<div className="details-cast-wrapper">
<h3>Cast</h3>
<div className="details-cast">
{currCast ?
<Swiper>
{currCast.map(person => (
<SwiperSlide>
<img className="cast-img"src={`https://image.tmdb.org/t/p/w220_and_h330_bestv2/${person.profile_path}`} />
// </SwiperSlide>
))}
</Swiper>
:
<h2>No cast</h2>
}
</div>
</div>
</div>
</div>
);
}
} export default MovieDetails
--- Last Update ---
Problem Solved!
I think the Swiper was initialized before anything was in it so the Swiper thought it had zero slides and wouldn't function. I fixed this by adding some conditional rendering to check the length of the movies variable. Initially, this did not work with the conditional rendering because I forgot to add the length check.
Before:
<h2 className="category-title">{genreCat}</h2>
{movies ?
<Swiper>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie}/>
</SwiperSlide>
))}
</Swiper>
:
<h2>No Movies</h2>
}
After:
<h2 className="category-title">{genreCat}</h2>
{movies.length > 0 ?
<Swiper>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie}/>
</SwiperSlide>
))}
</Swiper>
:
<h2>No Movies</h2>
}
Problem Solved! I think the Swiper was initialized before anything was in it so the Swiper thought it had zero slides and wouldn't function. I fixed this by adding some conditional rendering to check the length of the movies variable. Initially, this did not work with the conditional rendering because I forgot to add the length check.
Before:
<h2 className="category-title">{genreCat}</h2>
{movies ?
<Swiper>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie}/>
</SwiperSlide>
))}
</Swiper>
:
<h2>No Movies</h2>
}
After:
<h2 className="category-title">{genreCat}</h2>
{movies.length > 0 ?
<Swiper>
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<SwiperSlide key={movie.id}>
<MovieCard movie={movie}/>
</SwiperSlide>
))}
</Swiper>
:
<h2>No Movies</h2>
}
You should add "return"
movies.map((movie) => {
return <SwiperSlide></SwiperSlide>
}

Resources