when I lose in a hangman I want to reload my data in the API so that a new password appears. Unfortunately I have no idea how to reload it without reloading the whole page, is it even possible to run the api again on a button click for example?
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'axios';
const url = 'https://random-word-api.herokuapp.com/word?number=1';
const initialState = {
password: '1',
misstake: 0,
usedChart: [],
};
export const getPassword = createAsyncThunk(
'hangman/getPassword',
async (thunkAPI) => {
console.log(1)
try {
const resp = await axios(url)
return resp.data
} catch(error){
return thunkAPI.rejectWithValue('api not working');
}
}
);
const HangManSlice = createSlice({
name: 'hangman',
initialState,
reducers: {
increaseError: (state) => {
state.misstake += 1
},
usedCharts: (state, action) => {
state.usedChart.push(action.payload)
},
restart: (state) => {
state.misstake = 0
state.usedChart = []
getPassword()
}
},
extraReducers: (builder) => {
builder
.addCase(getPassword.fulfilled, (state, action) => {
state.password = action.payload;
})
}
})
export const { increaseError, usedCharts, restart } = HangManSlice.actions
export default HangManSlice.reducer
You should dispatch the getPassword() from the "restart" button handler instead of calling it your reducer.
const RestartButton = () => {
const dispatch = useDispatch();
const restartHandler = () => {
dispatch(getPassword());
dispatch(restart())
};
return (
<button onClick={restartHandler}>Restart</button>
);
}
Another option is to use the getPassword thunk to initialize a new game like so:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from 'axios';
const url = 'https://random-word-api.herokuapp.com/word?number=1';
const initialState = {
loading: false,
password: '1',
misstake: 0,
usedChart: [],
};
export const startGame = createAsyncThunk(
'hangman/getPassword',
async (thunkAPI) => {
console.log(1)
try {
const resp = await axios(url)
return resp.data
} catch(error){
return thunkAPI.rejectWithValue('api not working');
}
}
);
const HangManSlice = createSlice({
name: 'hangman',
initialState,
reducers: {
increaseError: (state) => {
state.misstake += 1
},
usedCharts: (state, action) => {
state.usedChart.push(action.payload)
},
},
extraReducers: (builder) => {
builder
.addCase(startGame.pending, (state, action) => {
state.loading = true;
})
.addCase(startGame.rejected, (state, action) => {
state.loading = false;
})
.addCase(startGame.fulfilled, (state, action) => {
state.loading = false;
// store new password
state.password = action.payload;
// reset state
state.misstake = 0
state.usedChart = []
})
}
})
export const { increaseError, usedCharts, restart } = HangManSlice.actions
export default HangManSlice.reducer
Now you can dispatch startGame(), and the state will be reset with a new password.
Related
I have a NextJS application that is using the ShopifyBuy SDK. I have been successfully able to implement a solution where I am able to fetch the products from Store and display them to the User. The user is also able to go to a product page and add the product to the cart.
However, when the user refreshes the page, the cart is reset, and the data does not persist. The code is below:
context/cart.js:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
import Cookies from "js-cookie";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState = {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const savedState = Cookies.get("cartState");
const [state, dispatch] = useReducer(reducer, savedState || initalState);
useEffect(() => {
Cookies.set("cartState", state, { expires: 7 });
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>
{children}
</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
products/[handle].tsx:
import React, { useState, useEffect } from "react";
import client from "../../lib/client";
import { useCartDispatch, useCartState } from "../../context/cart";
import Link from "next/link";
import cookie from "js-cookie";
export const getStaticPaths = async () => {
const res = await client.product.fetchAll();
const paths = res.map((product: any) => {
return {
params: { handle: product.handle.toString() },
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps = async (context: any) => {
const handle = context.params.handle;
const res = await client.product.fetchByHandle(handle);
const product = JSON.stringify(res);
return {
props: {
product,
},
};
};
function Product({ product }: any) {
const { state } = useCartState();
const { setCart } = useCartDispatch();
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
setCart(res);
};
product = JSON.parse(product);
return (
<div>
<div className=" flex-col text-2xl font-bold m-8 flex items-center justify-center ">
<h1>{product.title}</h1>
<button onClick={addToCart}>Add to Cart</button>
<Link href="/cart">Checkout</Link>
</div>
</div>
);
}
export default Product;
pages/cart/index.tsx:
import React, { useEffect } from "react";
import { useCartState, useCartDispatch } from "../../context/cart";
import client from "../../lib/client";
function Cart() {
const { state } = useCartState();
return (
<div>
<h1>Cart</h1>
{state.lineItems &&
state.lineItems.map((item: any) => {
return (
<div key={item.id}>
<h2>{item.title}</h2>
<p>{item.variant.title}</p>
<p>{item.quantity}</p>
</div>
);
})}
</div>
);
}
export default Cart;
I have tried using a library called js-cookie and also localStorage. I'm not sure where the problem lies or if the solutions that I've tried are wrong.
P.S.: I'm fairly new to NextJS and Typescript so go easy on the syntax. This code is for a personal project. Thanks in advance!
Answering this because I ended up coming up with a solution that works for me, at least.
Here it is:
const getCart = async () => {
try {
const checkoutId = Cookies.get("checkoutId");
let cart;
if (checkoutId) {
cart = await client.checkout.fetch(checkoutId);
} else {
cart = await client.checkout.create();
Cookies.set("checkoutId", cart.id);
}
setCart(cart);
} catch (err) {
console.log(err);
}
};
From my understanding, what this does is the following:
Check the cookies to see if one exists called "checkoutId"
If it exists, fetch the cart using that checkoutId
Otherwise, create a new cart and create a cookie using the cart.id that is returned in the response
Then, inside my individual Product page ([handle].tsx), I'm doing the following:
const addToCart = async () => {
const checkoutId = state.id;
const lineItemsToAdd = [
{
variantId: product.variants[0].id,
quantity: 1,
},
];
const res = await client.checkout.addLineItems(checkoutId, lineItemsToAdd);
console.log(res);
if (cookie.get("checkoutId") === undefined) {
cookie.set("checkoutId", res.id);
}
setCart(res);
};
Using cookies to store your object cart, as far as I know, is not a good idea. You could use localStorage, like so:
import { createContext, useContext, useEffect, useReducer } from "react";
import client from "../lib/client";
const CartStateContext = createContext();
const CartDispatchContext = createContext();
const SET_CART = "SET_CART";
const initalState =
typeof localStorage !== "undefined" && localStorage.getItem("cartState")
? JSON.parse(localStorage.getItem("cartState"))
: {
lineItems: [],
totalPrice: 0,
webUrl: "",
id: "",
};
const reducer = (state, action) => {
switch (action.type) {
case SET_CART:
return { ...state, ...action.payload };
default:
throw new Error(`Unknown action: ${action.type}`);
}
};
export const CartProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initalState);
useEffect(() => {
localStorage.set("cartState", JSON.stringify(state));
}, [state]);
useEffect(() => {
getCart();
}, []);
const setCart = (payload) => dispatch({ type: SET_CART, payload });
const getCart = async () => {
try {
const cart = await client.checkout.create();
setCart(cart);
} catch (err) {
console.log(err);
}
};
return (
<CartDispatchContext.Provider value={{ setCart }}>
<CartStateContext.Provider value={{ state }}>{children}</CartStateContext.Provider>
</CartDispatchContext.Provider>
);
};
export const useCartState = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);
I have two functions, one where I am able to send an order that updates the users balance amongst some other things, and another which retrieves the users balance for the user to see. Before any orders happen I still need to retrieve the balance for the user to see, thus I have broken my getBalance func from MarketLongFunc.
Using redux-toolkit and redux-thunk I have an ordersSlice.js that looks like this:
export const MarketLongFunc = createAsyncThunk(
"/order/marketlong",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
value: value,
token: token,
};
let url = `http://localhost:3001/api/orders/marketlong`;
const response = await axios.post(url, newObj);
//getBalance()
return;
}
);
export const getBalance = createAsyncThunk(
"/order/getBalance",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
token: token,
};
let url = `http://localhost:3001/api/orders/getBalance`;
const response = await axios.post(url, newObj);
return response.data;
}
);
const initialState = {
value: null,
error: null,
balance: null,
status: "idle",
orderStatus: "idle",
};
export const ordersSlice = createSlice({
name: "orders",
initialState,
reducers: {
reset: (state) => initialState,
resetStatus: (state) => {
state.orderStatus = "idle";
},
},
extraReducers(builder) {
builder
.addCase(MarketLongFunc.pending, (state, action) => {
state.orderStatus = "loading";
})
.addCase(MarketLongFunc.fulfilled, (state, action) => {
state.orderStatus = "success";
// getBalance();
// state.balance = action.payload;
})
.addCase(MarketLongFunc.rejected, (state, action) => {
state.orderStatus = "failed";
state.error = action.error.message;
})
.addCase(getBalance.pending, (state, action) => {
state.status = "loading";
})
.addCase(getBalance.fulfilled, (state, action) => {
// state.status = "success";
state.balance = action.payload;
state.status = "success";
})
.addCase(getBalance.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
},
});
export const { reset } = ordersSlice.actions;
export default ordersSlice.reducer;
Now in my next component the useEffect will call if there is no balance yet and the user is logged in. The way in which I was trying to solve my issue was to use the state.orderStatus = "success" under MarketLongFunc.fulfilled, this way hypothetically I can dispatch getbalance under the useEffect if a MarketLong is placed and then change the status with reset like the following:
export const Orderform = () => {
const user = useSelector((state) => state.auth.user);
const balance = useSelector((state) => state.orders.balance);
const status = useSelector((state) => state.orders.orderStatus);
const dispatch = useDispatch();
useEffect(() => {
if (!balance && user) {
dispatch(getBalance());
}
if (status == "success") {
dispatch(getBalance());
dispatch(resetStatus());
}
}, [balance]);
if (user) {
return (
<div>
<h1>
cash balance: ${balance ? Math.round(balance.balance) : "error"}
</h1>
<MarketLong />
</div>
);
}
return (
<div>
Login
</div>
);
};
The above code does not work currently as when I console.log(status) on refresh is is idle and when I use marketLong it is loading but it never makes it to fulfilled so still the only way to update the balance that is displayed after an order is to refresh the page. I want to update the displayed balance without refreshing the page as refreshing the page will have to make two other API calls on top of the getBalance. I have left some comments in where I tried things like just putting the getBalance function inside the MarketLongFunc in the ordersSlice, I also tried returning it etc but that did nothing and I figured fixing this issue in the useEffect with the status' would be the best way to fix this but I am open to hearing other solutions besides creating redundant code where I just basically type out the getBalance func inside marketLongFunc.
Another way that almost works is just adding dispatch(getBalance()) after dispatch(MarketLongFunc(longItem)); in my MarketLong react component like the following:
const addNewLong = async (e) => {
e.preventDefault();
const longItem = {
userInput: req.ticker,
quotePrice: req.quotePrice,
quantity: Item.quantity,
};
dispatch(MarketLongFunc(longItem));
dispatch(getBalance());
};
The problem with this is the first order never gets updated but after that it updates incorrectly as the balance will be off by one buy order. I imagine this is due to getBalance gettting called before MarketLongFunc but without setting a manual setTimeout func which seems like a clunky solution, I am not sure how to fix that with redux, you would think something like : if (dispatch(MarketLongFunc(longItem))) {dispatch(getBalance())}, but maybe this way needs to be changed in the ordersSlice (which I had tried and was not able to get it to work).
There are many ways to solve this problem - I will describe an approximate solution:
export const MarketLongFunc = createAsyncThunk(
"/order/marketlong",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
value: value,
token: token,
};
let url = `http://localhost:3001/api/orders/marketlong`;
const response = await axios.post(url, newObj);
//getBalance()
return;
}
);
export const getBalance = createAsyncThunk(
"/order/getBalance",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
token: token,
};
let url = `http://localhost:3001/api/orders/getBalance`;
const response = await axios.post(url, newObj);
return response.data;
}
);
const initialState = {
value: null,
error: null,
balance: null,
status: "idle",
orderStatus: "idle",
balanceNeedsToBeUpdated: true // <--- HERE
};
export const ordersSlice = createSlice({
name: "orders",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers(builder) {
builder
.addCase(MarketLongFunc.pending, (state, action) => {
state.orderStatus = "loading";
})
.addCase(MarketLongFunc.fulfilled, (state, action) => {
state.orderStatus = "idle";
state.balanceNeedsToBeUpdated = true; // < ----- HERE
// getBalance();
// state.balance = action.payload;
})
.addCase(MarketLongFunc.rejected, (state, action) => {
state.orderStatus = "failed";
state.error = action.error.message;
})
.addCase(getBalance.pending, (state, action) => {
state.status = "loading";
})
.addCase(getBalance.fulfilled, (state, action) => {
// state.status = "success";
state.balance = action.payload;
state.status = "idle";
state.balanceNeedsToBeUpdated = false; // <---- HERE
})
.addCase(getBalance.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
},
});
export const { reset } = ordersSlice.actions;
export default ordersSlice.reducer;
export const Orderform = () => {
const user = useSelector((state) => state.auth.user);
const balance = useSelector((state) => state.orders.balance);
const status = useSelector((state) => state.orders.status);
const orderStatus = useSelector((state) => state.orders.orderStatus);
const balanceNeedsToBeUpdated = useSelector((state) => state.orders.balanceNeedsToBeUpdated);
const dispatch = useDispatch();
useEffect(() => {
if (user && balanceNeedsToBeUpdated) { //< ----- HERE
dispatch(getBalance());
}
}, [user, balanceNeedsToBeUpdated]); // < ---- HERE
if (user) {
if (status == 'loading' || orderStatus == 'loading') {
return <div>loading</div>;
}
return (
<div>
<h1>
cash balance: ${balance ? Math.round(balance.balance) : "error"}
</h1>
<MarketLong />
</div>
);
}
return (
<div>
Login
</div>
);
};
//....
const addNewLong = async (e) => {
e.preventDefault();
const longItem = {
userInput: req.ticker,
quotePrice: req.quotePrice,
quantity: Item.quantity,
};
dispatch(MarketLongFunc(longItem)); // < --- HERE
};
I am trying to return the value from function that has the onSnapshot() event but keep getting this weird error. Basically, I call this action and return the data from it like I would in any other function. But I keep getting this error and I do not know how to fix it.
This is the error
Uncaught TypeError: Cannot add property 0, object is not extensible
at Array.push (<anonymous>)
This the function
export const getQuestions = () => {
var questions = [];
onSnapshot(collection(firebaseDatabase, "questions"), (querySnapshot) => {
querySnapshot.docs.forEach((doc) => {
if (doc.data() !== null) {
questions.push(doc.data());
}
});
});
return questions;
};
Also this function is used with Redux Thunk and Redux Toolkit.
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { getQuestions } from "../../utils/firebase-functions/firebase-functions";
export const getAllQuestions = createAsyncThunk(
"allQuestions/getAllQuestions",
async () => {
const response = getQuestions();
return response;
}
);
export const allQuestionsSlice = createSlice({
name: "allQuestions",
initialState: {
allQuestions: [],
loading: false,
error: null,
},
extraReducers: {
[getAllQuestions.pending]: (state) => {
state.loading = true;
state.error = null;
},
[getAllQuestions.fulfilled]: (state, action) => {
state.allQuestions = action.payload;
state.loading = false;
state.error = null;
},
[getAllQuestions.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload;
},
},
});
export default allQuestionsSlice.reducer;
Where it is dispatched
const dispatch = useDispatch();
const tabContentData = useSelector(
(state) => state.allQuestions.allQuestions
);
useEffect(() => {
dispatch(getAllQuestions());
}, [dispatch]);
console.log(tabContentData);
You can try returning a promise when the data is being fetch for first time as shown below:
let dataFetched = false;
export const getQuestions = () => {
return new Promise((resolve, reject) => {
onSnapshot(collection(firebaseDatabase, "questions"), (querySnapshot) => {
querySnapshot.docs.forEach((doc) => {
if (doc.data() !== null) {
questions.push(doc.data());
}
});
if (!dataFetched) {
// data was fetched first time, return all questions
const questions = querySnapshot.docs.map(q => ({ id: q.id, ...q.data()}))
resolve(questions)
dataFetched = true;
} else {
// Questions already fetched,
// TODO: Update state with updates received
}
});
})
};
getQuestions() now returns a Promise so add an await here:
const response = await getQuestions();
For updates received later, you'll have to update them directly in your state.
I'm totally new to the redux-toolkit and still learning it, I'm kind of blocked at this step as I don't know how to implement it with redux-toolkit.
I have a system of toasts build in my redux store and this was my action.
MY Action
const setAlert = (msg, alertType, timeout = 5000) => (dispatch) => {
const id = uuidv4();
dispatch({
type: SET_ALERT,
payload: { msg, alertType, id },
});
setTimeout(() => dispatch({ type: REMOVE_ALERT, payload: id }), timeout);
};
My Old reducer
export default function (state = initialState, action) {
const { type, payload } = action;
switch (type) {
case SET_ALERT:
return [...state, payload];
case REMOVE_ALERT:
return state.filter((alert) => alert.id !== payload);
default:
return state;
}
}
I was thinking about creating a react component to render when an alert array is longer than 0 with useEffect but I think it would be overkill.
I was thinking also about creating createAsyncThunk action but I need to return the value of the alert so I can't set setTimeout as the function would return.
Is there a way to get the dispatch function in the reducer so it would dispatch removeAlert after timeout?
const initialState: [] = [];
const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
setAlert(state, action) {
const id = uuidv4();
[...state, action.payload];
toast[action.payload.alertType](msg);
setTimeout(() => dispatch(removeAlert(id)), timeout);
},
removeAlert(state, action) {
return state.filter((alert) => alert.id !== action.payload);
},
},
});
I resolved it with hand-written thunk
https://redux.js.org/usage/writing-logic-thunks#async-logic-and-side-effects
const initialState: [] = [];
const alertSlice = createSlice({
name: 'alert',
initialState,
reducers: {
sA(state, action) {
[...state, action.payload];
toast[action.payload.alertType](action.payload.msg);
},
rA(state, action) {
return state.filter((a) => a.id !== action.payload.id);
},
},
});
export const { sA, rA } = alertSlice.actions;
export function alert(msg, alertType, timeout = 5000) {
return async (dispatch, getState) => {
const id = uuidv4();
const obj = {
msg,
alertType,
timeout,
id,
};
dispatch(sA(obj));
setTimeout(() => dispatch(rA(id)), timeout);
};
}
export default alertSlice;
I am using hooks and context api.I have multiple actions that write them into seperate file.my problem this:in another file how can I access state?
I use this file for create my contexts:
createContext.js
import React, { useReducer } from "react";
export default (reducer, actions, defaultValue) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, defaultValue);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};
and when I want to create context I pass actions ,reducer and default values to createContext file and get Context and Provider from that.like this:
productContext.js
import createDataContext from "./createDataContext";
import {storeProducts, detailProduct} from "../data";
const productReducer = (state, action) => {
switch (action.type) {
case "GET_ITEM":
return {...state, productDetail: action.productDetail};
case "ADD_TOTALS":
return {
...state,
cartSubTotal: action.cartSubTotal,
cartTotal: action.cartTotal,
cartTax: action.cartTax
};
case "ADD_TO_CART":
return {
...state,
products: action.tempProducts,
cart: [...state.cart, action.product]
};
default:
return state;
}
};
const getItem = (id) => {
const product = **products**.find(item => item.id === id);
return product;
}
const handleDetail = dispatch => (id) => {
const productDetail = getItem(id);
dispatch({type: "GET_ITEM", productDetail})
};
const addToCart = dispatch => (id) => {
let tempProducts = [...storeProducts];
const index = tempProducts.indexOf(getItem(id));
const product = tempProducts[index];
product.inCart = true;
product.count = 1;
const price = product.price;
product.total = price;
dispatch({
type: "ADD_TO_CART",
tempProducts,
product
});
const data = addTotals();
dispatch({
type: "ADD_TOTALS",
cartSubTotal: data.cartSubTotal,
cartTotal: data.cartTotal,
cartTax: data.cartTax
});
};
const addTotals = () => {
let subTotal = 0;
**cart**.map(item =>{ (subTotal += item.total)});
const tempTax = subTotal * 0.1;
const tax = parseFloat(tempTax.toFixed(2));
const total = subTotal + tax;
return {cartSubTotal: subTotal, cartTax: tax, cartTotal: total};
};
export const {Provider, Context} = createDataContext(
productReducer,
{
handleDetail,
},
{
products: storeProducts,
productDetail: detailProduct,
cart: [],
modalOpen: false,
modalProduct: detailProduct,
cartSubTotal: 0,
cartTax: 0,
cartTotal: 0
);
I can not access cart and products that are bold.how can I use them?
It looks like you're doing a lot of work in the action creator function that would make more sense as part of the reducer. For example, instead of this:
const productReducer = (state, action) => {
switch (action.type) {
case 'GET_ITEM':
return { ...state, productDetail: action.productDetail };
default:
return state;
}
};
const getItem = (id) => {
// no access to state!
const product = products.find((item) => item.id === id);
return product;
};
const handleDetail = (dispatch) => (id) => {
const productDetail = getItem(id);
dispatch({ type: 'GET_ITEM', productDetail });
};
You can do this:
// action value
{ type: 'GET_ITEM', id: 1234 }
// reducer
const productReducer = (state, action) => {
switch (action.type) {
case 'GET_ITEM':
const productDetail = state.products.find(
(item) => item.id === action.id
);
return { ...state, productDetail };
default:
return state;
}
};
Inside the reducer is where you have access to both the action and the state. Try to design your actions so that they contain the smallest amount of information possible to achieve your intention.