How can I better conditionally render my components? - reactjs

I have a lot of components being rendered based on different states that I'm using for a game. I'm currently using the method where I check the value of state with ampersands. I'm not sure if there's a different way I should do it, or if there's a more efficient cleaner way to do it.
I've looked up a few different ways but was wondering if someone could maybe give me suggestions for something that would work well with my code I have existing.
const App = () => {
const [startPlayer, setStartPlayer] = useState("");
const [endPlayer, setEndPlayer] = useState("");
const [gameSelected, setGameSelected] = useState(false);
const [gameStarted, setGameStarted] = useState(false);
const [gameWon, setGameWon] = useState(false);
const [winningTeam, setWinningTeam] = useState([]);
const [gameSolved, setGameSolved] = useState(false);
const isMobile = useMobileCheck();
const resetGame = () => {
setStartPlayer("");
setEndPlayer("");
setGameSelected(false);
setGameStarted(false);
setGameWon(false);
setGameSolved(false);
setWinningTeam([]);
};
const setGameType = (gameType) => {
setGameSelected(gameType);
};
const rollPlayers = (startYear, endYear) => {
axios.get(`/api/gettwoplayers?startYear=${startYear}&endYear=${endYear}`).then((res) => {
setStartPlayer(res.data[0]);
setEndPlayer(res.data[1]);
});
};
const startTheGame = () => {
setGameStarted(true);
};
const goBackToGameSelection = () => {
setGameSelected(false);
setGameStarted(false);
setStartPlayer("");
setEndPlayer("");
};
const userSetPlayer = (player, type) => {
if(type === "start") setStartPlayer(player);
if(type === "end") setEndPlayer(player);
};
const theGameWasWon = (history) => {
history.push(endPlayer);
setWinningTeam(history);
setGameWon(true);
};
const solveGame = () => {
setGameSolved(true);
axios.get(`/api/solve?startPlayer=${startPlayer}&endPlayer=${endPlayer}`).then((res) => {
console.log(res.data);
})
};
return (
<Container
sx={{
minHeight:'100vh',
maxWidth: "90vw!important",
}}
>
{
!gameSelected &&
!gameStarted &&
!gameWon &&
<ChooseGame setGameType={setGameType} />
}
{
!gameStarted &&
!gameWon &&
gameSelected === 'r' &&
<CreateRandomGame
rollPlayers={rollPlayers}
startPlayer={startPlayer}
endPlayer={endPlayer}
startTheGame={startTheGame}
goBack={goBackToGameSelection}
/>
}
{
!gameStarted &&
!gameWon &&
gameSelected === 's' &&
<CreateUserGame
startPlayer={startPlayer}
endPlayer={endPlayer}
userSetPlayer={userSetPlayer}
startTheGame={startTheGame}
goBack={goBackToGameSelection}
/>
}
{
!gameWon &&
gameStarted &&
<GameScreen
startPlayer={startPlayer}
endPlayer={endPlayer}
gameWon={theGameWasWon}
resetGame={resetGame}
solveGame={solveGame}
/>
}
{
gameWon &&
<GameWon
resetGame={resetGame}
winningTeam={winningTeam}
/>
}
</Container>
);
}
export default App;

Two things you could try:
Firstly, you've got a lot of boolean state - e.g. gameStarted, and a lot of it seems mutually-exclusive with other boolean state, for example gameStarted looks like it could never be true at the same time as gameWon. In situations like that, it can be a lot nicer to model the state as an enumerated type; unfortunately Javascript doesn't have them natively (look into TypeScript for a "true" enum type) but we can make do with strings:
const MODE_STARTED = 'started'
const MODE_SELECTED_RANDOM = 'random'
const MODE_SELECTED_USER = 'user'
const MODE_GAME_WON = 'won'
...
const [gameMode, setGameMode] = useState(MODE_STARTED);
...
Now rather than flipping individual booleans all over the place, you can just change your game mode ... e.g. setGameMode(MODE_SELECTED_RANDOM)
Once you've done that, your JSX can become cleaner too:
const showCorrectUI = () => {
switch (gameMode) {
case MODE_STARTED:
return <GameScreen {foo} />
case MODE_GAME_WON:
return <GameWon {bar} />
... // etc
}
}
return (
<Container
sx={{
minHeight:'100vh',
maxWidth: "90vw!important",
}}
>
{showCorrectUI()}
</Container>)

Related

React Component is rerendering, even though it does not depend on the state

In my React code I have to use a legacy component, which makes a setup api call when it is first rendered. The component has a custom completion/cancelation event which I use to trigger a State update. The current Code looks like this:
export const useOneTimePassword = (
headline = "OTP anfordern",
id = "opt",
type = "sms",
businessProcess = "otp-process"
): UseOneTimePasswordReturn => {
const [otpCode, setOtpCode] = useState<undefined | string>();
const [isOtpCancelled, setIsOtpCancelled] = useState<boolean>(false);
const openOtp = () => {
const otp = document.querySelector(`otp-component#${id}`) as OtpElement;
otp.open();
};
const OtpComponent: FC = () => (
<Otp
headline={headline}
id={id}
type={type}
businessProcess={businessProcess}
setIsOtpCancelled={setIsOtpCancelled}
setOtpCode={setOtpCode}
/>
);
return {
otpCode,
isOtpCancelled,
openOtp,
OtpComponent,
removeOtp: () => {
setOtpCode(undefined);
},
};
};
and for the Component it looks like this:
const Otp: React.FC<OtpProps> = ({
headline,
businessProcess,
type,
id,
setOtpCode,
setIsOtpCancelled,
}) => {
function onOtpResponse(e: CompletedEvent) {
if (e.detail.otpCode) {
setOtpCode(e.detail.otpCode);
setIsOtpCancelled(false);
} else {
setIsOtpCancelled(true);
}
}
const ref = useRef();
useEffect(() => {
//#ts-ignore
if (ref.current) ref.current.addEventListener("completed", onOtpResponse);
}, []);
return (
<otp-component
ref={ref}
headline={headline}
id={id}
type={type}
business-process={businessProcess}
/>
);
};
export default Otp;
What I do not understand is that state changes in otpCode aswell as isOtpCancelled cause a rerender of the OtpComponent

Why do I get maximum update depth exceeded for rendered screen?

I get an error for one component that Maximum update depth exceeded, I tried to figure out what cause the issue and I fail. Can You please look at the console comment and the code and suggest where shall I search for problem solving ?
export const LineAlertsScreen = () => {
const { useTranslationFunc } = useTranslationFacade();
const { lineAlertsData, getLineAlertsData } =
useContext(LineDetailsContext);
const { handlePushToProperLocationDependingOnAlertValues } =
useContext(AlertsContext);
const { lineId } = useParams<RouterParamsModel>();
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('md'));
useEffect(() => {
if (lineId) {
getLineAlertsData(lineId);
}
}, [lineId]);
const dateTranslation = useTranslationFunc('Data');
const lineTranslation = useTranslationFunc('Linia');
const typeTranslation = useTranslationFunc('Typ');
const lineKmTranslation = useTranslationFunc('Kilometr');
const descriptionTranslation = useTranslationFunc('Opis');
return (
<div>
<LinesGlobalToolbar />
<StyledAlertsContentWrapper>
{matches && <SideNavigationPanelLines />}
<StyledBackgroundGreyDiv>
<StyledBackgroundContainerDiv>
<LineHeader lineId={lineId} />
<StyledBackgroundContentContainerDiv>
<StyledHeaderDiv>
{useTranslationFunc('Lista alertów')}
</StyledHeaderDiv>
<AlertList
columns={columns}
data={lineAlertsData ?? []}
onRowClick={
handlePushToProperLocationDependingOnAlertValues
}
/>
</StyledBackgroundContentContainerDiv>
</StyledBackgroundContainerDiv>
</StyledBackgroundGreyDiv>
</StyledAlertsContentWrapper>
{!matches && <LinesBottomNavigationMobile />}
</div>
);
};

Why is my property always wrong, even though it gets changed and is a dependency?

I have these properties declared in my app:
const [lockfileData, setLockFileData] = useState({});
const [socket, setSocket] = useState<RiotWSProtocol>(null);
const [api, setApi] = useState<LoLAPI>(null);
const [champions, setChampions] = useState<Champion[]>([]);
const [summoner, setSummoner] = useState<Summoner>(null);
const [autoAcceptQueue, setAutoAccept] = useState(true);
const [instalockEnabled, setEnableInstalock] = useState(true);
const [selectedChampion, setSelectedChampion] = useState<Champion>(null);
const [callRoleEnabled, setCallRoleEnabled] = useState(true);
const [selectedRole, setSelectedRole] = useState<Role>('Mid');
I have an event handler in my useEffect hook, and inside that it handles more events:
const onJsonApiEvent = useCallback(
(message: any) => {
//console.log(message);
if (
message.uri === '/lol-matchmaking/v1/ready-check' &&
autoAcceptQueue
) {
if (
message.data?.state === 'InProgress' &&
message.data?.playerResponse !== 'Accepted'
) {
api.acceptQueue();
}
} else if (
message.uri === '/lol-champ-select/v1/session' &&
message.eventType === 'Update'
) {
console.log('enabled?', instalockEnabled)
if (instalockEnabled) {
const myCellId = message.data.localPlayerCellId as number;
const myAction = (message.data.actions[0] as any[]).find(
(x) => x.actorCellId === myCellId
);
if (
!myAction.completed &&
myAction.isInProgress &&
myAction.type === 'pick'
) {
api.pickAndLockChampion(1, myAction.id);
}
console.log('myAction', myAction);
}
}
},
[api, autoAcceptQueue, instalockEnabled]
);
const onSocketOpen = useCallback(() => {
console.log('socket', socket);
if (socket) {
socket.subscribe('OnJsonApiEvent', onJsonApiEvent);
}
}, [onJsonApiEvent, socket]);
const onConnect = useCallback((data: LCUCredentials) => {
setLockFileData(data);
const lolApi = new LoLAPI(data);
setApi(lolApi);
lolApi.getOwnedChampions().then((champs) => {
setSelectedChampion(champs[0]);
setChampions(champs);
});
lolApi.getCurrentSummoner().then((summoner) => {
setSummoner(summoner);
});
const wss = new RiotWSProtocol(
`wss://${data.username}:${data.password}#${data.host}:${data.port}`
);
setSocket(wss);
}, []);
useEffect(() => {
if (socket) {
socket.on('open', onSocketOpen);
}
connector.on('connect', onConnect);
connector.start();
return () => {
connector.stop();
};
}, [onConnect, onSocketOpen, socket]);
The dependencies appear to be correct, so it should be using the up to date values in each handler.
However, inside the onJsonApiEvent handler, properties such as instalockEnabled are always the default value.
I am updating the value of instalockEnabled in a component on my page:
<FormControlLabel
control={
<Checkbox
checked={instalockEnabled}
name="instalockEnabled"
color="primary"
onChange={handleInstalockEnableChange}
/>
}
label="Enabled"
/>
And its handler looks like this:
const handleInstalockEnableChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setEnableInstalock(e.target.checked);
};
How come this is happening when it is a dependency?
The janky solution I've come up with for now is to have a separate variable that is useRef and update that at the same time as updating the state, therefore it persists:
const [instalockEnabled, setEnableInstalock] = useState(true);
const instalockEnabledRef = useRef(instalockEnabled);
const handleInstalockEnableChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setEnableInstalock(e.target.checked);
instalockEnabledRef.current = e.target.checked;
};
And then just use instalockEnabledRef.current inside of the event handlers where it needs to know the current value.

Multiple filters with React Hook - Chaining function

Hello I have created a search bar with a multipl filter, it works but the functions are too dependent on each other. The problem here is that the functions are handling multiple cases.
would it be possible to lighten each function by chaining them and how ? I don't really get chaining method.
thanks
import React, { useState, useEffect } from "react";
import Search from "./Search";
import Anime from "./Anime";
import "./App.css";
const KIJAN_API_URL = "https://api.jikan.moe/v3/top/anime/1/upcoming";
const App = () => {
const [animes, setAnimes] = useState([]);
const [sortedAnimes, setSortedAnimes] = useState([]);
const [searchValue, setSearchValue] = useState("")
const [filterByType, setFilterByType] = useState("");
const [filterByYear, setFilterByYear] = useState("");
useEffect(() => {
fetch(KIJAN_API_URL)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error("Something went wrong");
}
})
.then(jsonResponse => {
setAnimes(jsonResponse.top);
})
.catch(error => {
console.log(error);
});
}, []);
useEffect(() => {
const callFilterByType = result => {
if (filterByType === "") {
callFilterByYear(result);
console.log(result);
} else {
result = result.filter(anime => anime.type === filterByType);
callFilterByYear(result);
console.log(result);
}
};
const callFilterByYear = result => {
if (filterByYear === "") {
setSortedAnimes(result);
} else {
const regex = new RegExp(`${filterByYear}`, "gi");
result = result.filter(anime => regex.test(anime.start_date));
setSortedAnimes(result);
console.log(result);
}
};
if (searchValue === "") {
callFilterByType(animes);
} else {
const regex = new RegExp(`${searchValue}`, "gi");
console.log("search : ", searchValue);
const result = animes.filter(anime => regex.test(anime.title));
callFilterByType(result);
console.log(result);
}
}, [searchValue, animes, filterByType, filterByYear]);
return (
<div className="App">
<Search
searchValue={searchValue}
setSearchValue={setSearchValue}
filterByType={filterByType}
setFilterByType={setFilterByType}
filterByYear={filterByYear}
setFilterByYear={setFilterByYear}
/>
{sortedAnimes.length > 0 ? (
sortedAnimes.map((anime, index) => {
return <Anime key={index} anime={anime} />;
})
) : (
<span>Aucune correspondance</span>
)}
</div>
);
};
export default App;
SandBox Sample
You can do first round of simplification like this:
useEffect(() => {
let result = [...animes];
if(searchValue) {
const searchRegex = new RegExp(`${searchValue}`, "gi");
result = result.filter(anime => searchRegex.test(anime.title));
}
if(filterByType) {
result = result.filter(anime => anime.type === filterByType);
}
if(filterByYear) {
const yearRegex = new RegExp(`${filterByYear}`, "gi");
result = result.filter(anime => yearRegex.test(anime.start_date));
}
setSortedAnimes(result);
}, [searchValue, animes, filterByType, filterByYear]);
It can be reduced to more compact form, like:
useEffect(() => {
const searchRegex = searchValue && new RegExp(`${searchValue}`, "gi");
const yearRegex = filterByYear && new RegExp(`${filterByYear}`, "gi");
const result = animes.filter(anime =>
(!searchRegex || searchRegex.test(anime.title)) &&
(!filterByType || anime.type === filterByType)) &&
(!yearRegex || yearRegex.test(anime.start_date))
)
setSortedAnimes(result);
}, [searchValue, animes, filterByType, filterByYear]);
More idiomatic way would be use use momoisation hook. i.e. Remove sortedAnimes as state and
const sortedAnimes = useMemo(() => {
const searchRegex = searchValue && new RegExp(`${searchValue}`, "gi");
const yearRegex = filterByYear && new RegExp(`${filterByYear}`, "gi");
return animes.filter(anime =>
(!searchRegex || searchRegex.test(anime.title)) &&
(!filterByType || anime.type === filterByType)) &&
(!yearRegex || yearRegex.test(anime.start_date))
)
}, [searchValue, animes, filterByType, filterByYear]);
try this
if you are using filter method inside jsx then you try this method.
Let me brief it,
consider userInfo like an object containing fields like name, email, location etc. so, if you want your filter method to provide your search results based on these fields value then you can use something like this in jsx.
{userInfo.filter((user) => (
user.name.toLowerCase().includes(cloneSearchTerm)
||
user.email.toLowerCase().includes(cloneSearchTerm)
||
user.location.toLowerCase().includes(cloneSearchTerm)
)
).map((user, idx) => (
<div key={idx}>
<span>{user.name}</span>
<span>{user.email}</span>
<span>{user.location}</span>
</div>
))}

When to use hooks? is worth it that example?

I have write a hook to check if browser is IE, so that I can reutilize the logic instead of write it in each component..
const useIsIE = () => {
const [isIE, setIsIE] = useState(false);
useEffect(() => {
const ua = navigator.userAgent;
const isIe = ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
setIsIE(isIe);
}, []);
return isIE;
}
export default useIsIE;
Is it worth it to use that hook?
Im not sure if is good idea because that way, Im storing a state and a effect for each hook call (bad performane?) when I can simply use a function like that:
export default () => ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
What do you think? is worth it use that hook or not?
If not, when should I use hooks and when not?
ty
No. Not worth using the hook.
You'd need to use a hook when you need to tab into React's underlying state or lifecycle mechanisms.
Your browser will probably NEVER change during a session so just creating a simple utility function/module would suffice.
I would recommend to set your browser checks in constants and not functions, your browser will never change.
...
export const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor);
export const isIOSChrome = /CriOS/.test(userAgent);
export const isMac = (navigator.platform.toUpperCase().indexOf('MAC') >= 0);
export const isIOS = /iphone|ipad|ipod/.test(userAgent.toLowerCase());
...
This is a simple hook that checks if a element has been scrolled a certain amount of pixels
const useTop = (scrollable) => {
const [show, set] = useState(false);
useEffect(() => {
const scroll = () => {
const { scrollTop } = scrollable;
set(scrollTop >= 50);
};
const throttledScroll = throttle(scroll, 200);
scrollable.addEventListener('scroll', throttledScroll, false);
return () => {
scrollable.removeEventListener('scroll', throttledScroll, false);
};
}, [show]);
return show;
};
Then you can use it in a 'To Top' button to make it visible
...
import { tween } from 'shifty';
import useTop from '../../hooks/useTop';
// scrollRef is your scrollable container ref (getElementById)
const Top = ({ scrollRef }) => {
const t = scrollRef ? useTop(scrollRef) : false;
return (
<div
className={`to-top ${t ? 'show' : ''}`}
onClick={() => {
const { scrollTop } = scrollRef;
tween({
from: { x: scrollTop },
to: { x: 0 },
duration: 800,
easing: 'easeInOutQuart',
step: (state) => {
scrollRef.scrollTop = state.x;
},
});
}}
role="button"
>
<span><ChevronUp size={18} /></span>
</div>
);
};

Resources