I've set up a new project with React, Redux (using toolkit). I've got a button that needs to be disabled if the user does not have enough of a particular resource. I've confirmed that the state is being updated properly and the reducers are applying to state properly, but I am unable to get the button to disable when that resource falls below the supplied price.
I've tried duplicating state from redux using a useState hook, but setting the state within canAfford() still doesn't disable the button. I'm at a bit of a loss, and feel like I'm just missing something about redux state and rendering.
Here's the button component I'm working with:
function BuyBtn({ technology, label, resourceType, price, requirements = []}: IBuyBtn) {
const units = useSelector((state: any) => state.units);
const tech = useSelector((state: any) => state.tech);
const resources = useSelector((state: any) => state.resources);
const dispatch = useDispatch();
let disabled = false;
let unlocked = true;
useEffect(() => {
disabled = !canAfford()
}, [resources])
const canAfford = (): boolean => {
console.log('Units:', units);
console.log("Checking affordability");
if (resourceType.length != price.length) {
throw `BuyBtn Error: price length is ${price.length} but resource length is ${resourceType.length}.`;
}
resourceType.forEach((res, i) => {
const resPrice = price[i];
if (resources[res] < resPrice) {
return false;
}
});
return true;
};
const meetsRequirements = (): boolean => {
if (requirements.length === 0) {
return true;
}
requirements.forEach((req) => {
if (!tech[req]) {
return false;
}
});
return true;
};
const buyThing = () => {
if (canAfford() && meetsRequirements()) {
resourceType.forEach((res, i) => {
const resPrice = price[i];
dispatch(SubtractResource(res, resPrice));
});
dispatch(UnlockTech(technology, true))
}
};
if (meetsRequirements() && canAfford()) {
return (
<button onClick={buyThing} disabled={disabled}>{label}</button>
);
} else {
return null;
}
}
export default BuyBtn;
Instead of using disabled as variable make it State which will trigger re-render:
function BuyBtn({ technology, label, resourceType, price, requirements = []}: IBuyBtn) {
const units = useSelector((state: any) => state.units);
const tech = useSelector((state: any) => state.tech);
const resources = useSelector((state: any) => state.resources);
const dispatch = useDispatch();
const [disabled, setDisabled] = React.useState(false);
let unlocked = true;
const canAfford = (): boolean => {
console.log('Units:', units);
console.log("Checking affordability");
if (resourceType.length != price.length) {
throw `BuyBtn Error: price length is ${price.length} but resource length is ${resourceType.length}.`;
}
let isAffordable = true
resourceType.forEach((res, i) => {
const resPrice = price[i];
if (resources[res] < resPrice) {
isAffordable = false;
}
});
return isAffordable;
};
useEffect(async() => {
const value = await canAfford();
setDisabled(!value);
}, [resources])
const meetsRequirements = (): boolean => {
if (requirements.length === 0) {
return true;
}
let isMeetingRequirements = true;
requirements.forEach((req) => {
if (!tech[req]) {
isMeetingRequirements = false;
}
});
return isMeetingRequirements;
};
const buyThing = () => {
if (canAfford() && meetsRequirements()) {
resourceType.forEach((res, i) => {
const resPrice = price[i];
dispatch(SubtractResource(res, resPrice));
});
dispatch(UnlockTech(technology, true))
}
};
if (meetsRequirements() && canAfford()) {
return (
<button onClick={buyThing} disabled={disabled}>{label}</button>
);
} else {
return null;
}
}
export default BuyBtn;
Related
I have this component:
const updateUrl = (url: string) => history.replaceState(null, '', url);
// TODO: Rename this one to account transactions ATT: #dmuneras
const AccountStatement: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { virtual_account_number: accountNumber, '*': transactionPath } =
useParams();
const [pagination, setPagination] = useState<PaginatorProps>();
const [goingToInvidualTransaction, setGoingToInvidualTransaction] =
useState<boolean>(false);
const SINGLE_TRANSACTION_PATH_PREFIX = 'transactions/';
// TODO: This one feels fragile, just respecting what I found, but, we could
// investigate if we can jsut rely on the normal routing. ATT. #dmuneras
const transactionId = transactionPath?.replace(
SINGLE_TRANSACTION_PATH_PREFIX,
''
);
const isFirst = useIsFirstRender();
useEffect(() => {
setGoingToInvidualTransaction(!!transactionId);
}, [isFirst]);
const {
state,
queryParams,
dispatch,
reset,
setCursorAfter,
setCursorBefore
} = useLocalState({
cursorAfter: transactionId,
includeCursor: !!transactionId
});
const {
filters,
queryParams: globalQueryParams,
setDateRange
} = useGlobalFilters();
useUpdateEffect(() => {
updateUrl(
`${location.pathname}?${prepareSearchParams(location.search, {
...queryParams,
...globalQueryParams
}).toString()}`
);
}, [transactionId, queryParams]);
useUpdateEffect(() => dispatch(reset()), [globalQueryParams]);
const account_number = accountNumber;
const requestParams = accountsStateToParams({
account_number,
...state,
...filters
});
const { data, isFetching, error, isSuccess } =
useFetchAccountStatementQuery(requestParams);
const virtualAccountTransactions = data && data.data ? data.data : [];
const nextPage = () => {
dispatch(setCursorAfter(data.meta.cursor_next));
};
const prevPage = () => {
dispatch(setCursorBefore(data.meta.cursor_prev));
};
const onRowClick = (_event: React.MouseEvent<HTMLElement>, rowData: any) => {
if (rowData.reference) {
if (rowData.id == transactionId) {
navigate('.');
} else {
const queryParams = prepareSearchParams('', {
reference: rowData.reference,
type: rowData.entry_type,
...globalQueryParams
});
navigate(
`${SINGLE_TRANSACTION_PATH_PREFIX}${rowData.id}?${queryParams}`
);
}
}
};
const checkIfDisabled = (rowData: TransactionData): boolean => {
return !rowData.reference;
};
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
const showTransactionsTable: boolean =
Array.isArray(virtualAccountTransactions) && isSuccess && data?.data;
const onTransactionSourceLoaded = (
transactionSourceData: PayoutDetailData
) => {
const isIncludedInPage: boolean = virtualAccountTransactions.some(
(transaction: TransactionData) => {
if (transactionId) {
return transaction.id === parseInt(transactionId, 10);
}
return false;
}
);
if (!goingToInvidualTransaction || isIncludedInPage) {
return;
}
const fromDate = dayjs(transactionSourceData.timestamp);
const toDate = fromDate.clone().add(30, 'day');
setDateRange({
type: 'custom',
to: toDate.format(dateFormat),
from: fromDate.format(dateFormat)
});
setGoingToInvidualTransaction(false);
};
const fromDate = requestParams.created_after || dayjs().format('YYYY-MM-DD');
const toDate = requestParams.created_before || dayjs().format('YYYY-MM-DD');
const routes = [
{
index: true,
element: (
<BalanceWidget
virtualAccountNumber={account_number}
fromDate={fromDate}
toDate={toDate}
/>
)
},
{
path: `${SINGLE_TRANSACTION_PATH_PREFIX}:transaction_id`,
element: (
<TransactionDetails
onTransactionSourceLoaded={onTransactionSourceLoaded}
/>
)
}
];
return (........
I get this error: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
The useEffect where the issue is, it is this one:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
Considering previous answers, would the solution be to make sure I return a new object each time? But I am not sure what would be the best approach. Any clues ?
did you want the useEffect to start every changes of 'data?.meta' ?
Without reading all the code, I believe the data.meta object changes on every render. There is a way to change the useEffect to narrow done its execution conditions:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [!data?.meta, data?.meta?.has_previous_page, data?.meta?.has_next_page]);
Please note the ! before data.?.meta which makes the hook test only for presence or absence of the object, since your code doesn't need more than that information.
Please can someone tell me what is such as react component that returns a bunch of functions and which uses react hooks inside of it ?
Even a react functional component is supposed to return jsx or any other template , no ?
Is such as component stated in react documentation ? how is it called ?!
export default function useViewFilter() {
const dispatch = useDispatch();
const { boardId, viewId } = useParams<RouteParamsTypes>();
const [selectedFacets, setSelectedFacets] = useState<SelectedFacets>({});
const [propertiesData, setPropertiesData] = useState<PropertyData[]>([]);
const [propertiesFacetingTypes, setPropertiesFacetingTypes] = useState<PropertiesFacetingTypes>({});
const neededData = useSelector((state: IFrontAppState) => selectFilterHookNeededData(state, { boardId, viewId }));
const isReady = useSelector((state: IFrontAppState) => isViewModelLoaded(state, boardId, viewId));
const storeFacetsFilters = useSelector((state: IFrontAppState) => getViewFacetsFilters(state, boardId, viewId));
const storeFacetingTypes = useSelector((state: IFrontAppState) =>
getViewPropertiesFacetingTypes(state, boardId, viewId)
);
const dataRef = useRef({
initialized: false
});
useEffect(() => {
handleMakeNewSearch(selectedFacets, false);
}, [neededData]);
// last state before refresh
useEffect(() => {
if (isReady && !dataRef.current.initialized) {
dataRef.current.initialized = true;
setSelectedFacets(storeFacetsFilters ? (storeFacetsFilters as SelectedFacets) : {});
setPropertiesFacetingTypes(storeFacetingTypes ? storeFacetingTypes : {});
handleMakeNewSearch(storeFacetsFilters as SelectedFacets, false);
}
}, [isReady, storeFacetsFilters, storeFacetingTypes, dataRef]);
const handleClear = () => {
const selectedFacets = {};
setSelectedFacets(selectedFacets);
handleMakeNewSearch(selectedFacets, true);
dispatch(
actions.ClearViewFilters({
BoardId: boardId,
ViewId: viewId
})
);
};
const handleMakeNewSearch = (selectedFacets: SelectedFacets, doUpdateInBackend: boolean) => {
const { propertiesIds, properties, orderedEntities, boardMembersObj } = neededData;
if (properties && propertiesIds?.length) {
const orderedProperties = propertiesIds?.map(pId => ({
Id: pId,
...properties[pId]
}));
const itemsJsData = makeItemsJsDataFromAllEntities(orderedEntities, orderedProperties, boardMembersObj);
const newSearchResult = makeSearchAggregations(itemsJsData, orderedProperties, selectedFacets);
// set filtred items
if (doUpdateInBackend) {
const filtredEntitiesIds = newSearchResult.items.map(x => x.id) as string[];
dispatch(
actions.UpdateViewFilters({
BoardId: boardId,
ViewId: viewId,
FacetsFilters: selectedFacets,
FiltredEntitiesIds: doesThereIsFeltering(selectedFacets) ? filtredEntitiesIds : null,
PropertiesFacetingTypes: propertiesFacetingTypes
})
);
}
// update filter panel
const newPropertiesData = makeNewPropertiesData(
propertiesIds,
properties,
newSearchResult.aggregations,
propertiesFacetingTypes
);
setPropertiesData(newPropertiesData);
}
};
return {
propertiesData,
propertiesFacetingTypes,
handleClear,
setPropertiesFacetingTypes
};
}
It's a custom hook.
it's one of the ways (nowadays maybe the standard way) of extracting logic in React function components.
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.
Task:
There is a field (grid) along which the snake's head moves, when you click on the arrows, the head changes direction.
Rendering should take place according to the initial speed (and it can also change depending on conditions, it is not included in the context of this request), the speed is calculated in seconds / milliseconds.
All code works fine except for the problem (GameSleep method) with managing this same render delay until the next step (render).
Who can help with setting up a lifecycle that suits the original task?
main screen
p.s. Sorry for the long listing, could not hide in the spoiler
CODE
Index.js:
ReactDOM.render(<Provider store={store}><Snake/></Provider>, document.getElementById('root'));
Snake.js:
const Snake = (props) => {
const [process, setProcess] = useReducer(
ReducerProcess, {start: false, victory: false, gameover: false, reset: true }
);
const [direction, setDirection] = useState(undefined);
const SnakeEventListener = useCallback((event) => {
if( checkKey(event.code) ) {
const direction = getDirection(event.code);
setDirection(direction);
}
}, []);
const onProcess = useCallback((inType) => {
switch(inType) {
case STATE.reset:
setDirection(undefined); break;
case STATE.start:
document.addEventListener('keydown', SnakeEventListener, false); break;
case STATE.victory:
document.removeEventListener('keydown', SnakeEventListener, false); break;
case STATE.gameover:
document.removeEventListener('keydown', SnakeEventListener, false); break;
default: break;
}
setProcess({type: inType});
}, [SnakeEventListener]);
return (
<div>
<Grid process={process} toProcess={onProcess} direction={direction} toDirection={setDirection}/>
<button onClick={()=>onProcess('reset')}>Reset</button>
<button onClick={()=>onProcess('start')}>Start</button>
</div>
);
}
Grid.js:
const Grid = (props) => {
const {process, toProcess, direction, toDirection} = props;
const [snake, setSnake] = useState(undefined);
const [grid, setGrid] = useState(undefined);
// Effect => Initial
useEffect(() => {
const Initial = () => {
setSnake( initSnake() );
toDirection( initDirection() );
setGrid( InitGrid() );
}
if(process.reset && !direction) { Initial(); }
}, [process, direction, toDirection]);
// Effect => Game Loop
useEffect(() => {
const GameLoop = () => {
const build = BuildGrid(snake, direction);
if(build.victory || build.gameover) {
if(build.victory) { toProcess('victory'); }
if(build.gameover) { toProcess('gameover'); }
}else{
setSnake(build.snake);
setGrid(build.grid);
}
}
if(process.start && direction) { GameLoop(); }
}, [process, toProcess, direction, snake]);
return (<div>{ Cells(grid) }</div>);
}
Building.js:
const BuildGrid = (input) => {
const content = Execute(input);
return content;
}
const Execute = async (input) => {
await GameSleep(input.speed);
let newSnake = input.snake;
… … …
return { snake: newSnake, victory: victory, gameover: gameover };
}
const GameSleep = async (delay) => {
return new Promise(resolve => setTimeout(resolve, delay));
}
You can try a clearTimeout(timeoutName). You can create a function that clears the timeout.
let gameSleepTimeout;
const GameSleep = (delay) => {
gameSleepTimeout = setTimeout(() => console.log('setTimeout callback'), delay);
};
const clearGameSleep = () => clearTimeout(gameSleepTimeout);
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>
);