Update client state received from api without refreshing in redux - javascript

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
};

Related

How to reload data from api in Redux

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.

Firebase - return the value from the onSnapshot event in function

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.

Connecting Term to a Redux dispatch Action

I have created an input field with a search term which creates a request to a backend API. To summarise, two issues:
It fetches data from my API, but it fetches ALL roles, not just ones filtered by the term.
It does not commit to the redux store.
Please see my app, it contains simply:
This is my frontend component, which is making an action dispatch based on a search term.
export function SearchBarTrialRedux(props) {
const [isExpanded, setExpanded] = useState(false);
const [parentRef, isClickedOutside ] = useClickOutside();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setLoading] = useState(false);
const [jobPostings, setjobPostings] = useState([]);
const [noRoles, setNoRoles] = useState(false)
const isEmpty = !jobPostings || jobPostings.length === 0;
const expandedContainer = () => {
setExpanded(true);
}
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoRoles(false);
if (inputRef.current) inputRef.current.value = "";
};
useEffect(()=> {
if(isClickedOutside)
collapseContainer();
}, [isClickedOutside])
const [term, setTerm] = useState("")
const dispatch = useDispatch();
const changeHandler = (e) => {
e.preventDefault();
fetchAsyncRoles(dispatch, {term});
}
return (
<SearchBarContainer animate = {isExpanded ? "expanded" : "collapsed"}
variants={containerVariants} transition={containerTransition} ref={parentRef}>
<SearchInputContainer>
<SearchIconSpan>
<SearchIcon/>
</SearchIconSpan>
<form onSubmit={changeHandler}>
<SearchInput placeholder = "Search for Roles"
onFocus={expandedContainer}
ref={inputRef}
value={term}
onChange={(e)=> setTerm(e.target.value)}
/>
</form>
</SearchBarContainer>
And my jobsearchSlice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { publicRequest } from "../requestMethods";
export const fetchAsyncRoles = async (dispatch, term) => {
dispatch(searchStart());
try {
const res = await publicRequest.get(`http://localhost:5000/api/role/titlerole?title=${term}`);
dispatch(searchSuccess(res.data));
console.log(res.data)
} catch (err) {
dispatch(searchFailure());
}
};
const jobsearchSlice = createSlice({
name: "jobsearchSlice",
initialState: {
isFetching: false,
roles: [],
error: false,
},
reducers: {
searchStart: (state) => {
state.isFetching = true;
},
searchSuccess: (state, action) => {
state.isFetching = false;
state.roles = action.payload;
},
searchFailure: (state) => {
state.isFetching = false;
state.error = true;
},
},
});
export const { searchStart, searchSuccess, searchFailure } = jobsearchSlice.actions;
export default jobsearchSlice.reducer;
As stated, it does create and fetch this data. This does commit it to the store under the roles key, which is great! That's what I want, however it is not filtering. E.g If we look at a role specifically like Data Scientist:
https://gyazo.com/ca4c2b142771edd060a7563b4200adf8
I should be getting just 1 key, Data Scientist.
Looking at the backend of the console.log(res), I can see that it appears my term isn't properly coming through and filtering my roles :
responseURL: "http://localhost:5000/api/role/titlerole?title=[object%20Object]"
But if I log the term, it does come through exactly as input.
What's wrong, what am I doing and how should I solve this term flowing through to filter my req?
I can confirm that if I do this on postman it works...
https://gyazo.com/10f2946c1a3807370b4792c06292b557

Redux toolkit alert removal setTimeout

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;

How do you get an element out of a redux reducer?

I have a react component that has a html button that when clicked calls a function that adds an element to a redux reducer and then redirects to another component. The component that is redirected to needs to set state from the reducer but it won't. I know that it is being added to the array in the reducer because I wrote it as an async await and it redirects after it gets added.
This is the original component
const Posts = () => {
const dispatch = useDispatch();
const getProfile = async (member) => {
await dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
window.location.href='/member'
console.log('----------- member------------')
console.log(post)
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default Posts;
This is the 'socialNetworkContract' reducer
import { connect, useDispatch, useSelector } from "react-redux";
let init = {
posts:[],
post:{},
profiles:[],
profile:{},
members:[],
member:{}
}
export const socialNetworkContract = (state = init, action) => {
const { type, response } = action;
switch (type) {
case 'ADD_POST':
return {
...state,
posts: [...state.posts, response]
}
case 'SET_POST':
return {
...state,
post: response
}
case 'ADD_PROFILE':
return {
...state,
profiles: [...state.profiles, response]
}
case 'SET_PROFILE':
return {
...state,
profile: response
}
case 'ADD_MEMBER':
return {
...state,
members: [...state.members, response]
}
case 'SET_MEMBER':
return {
...state,
member: response
}
default: return state
}
};
and this is the component that the html button redirects to
const Member = () => {
const [user, setUser] = useState({})
const [profile, setProfile] = useState({});
const dispatch = useDispatch();
useEffect(async()=>{
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div class="container">
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
{p.message}
{p.replies}
</tr>})}
</div>
</div>
</div>
</div>
)
}
export default Member;
This is the error I get in the console
Error: invalid address (arg="user", coderType="address", value={})
The functions I'm calling are solidity smart contracts and the have been tested and are working and the element I'm trying to retrieve out of the array is an ethereum address.
incidentsInstance and snInstance are declared in the try statement but I took a lot of the code out to make it easier to understand.
given setUser is async, your user is still an empty object when you make your request.
you could pass pro value instead:
useEffect(async () => {
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
const a = await snInstance.getUsersPosts(pro, { from: accounts[0] });
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
or break your useEffect in two pieces:
useEffect(() => {
setUser(socialNetworkContract.members[0]);
}, [])
useEffect(async () => {
if (!Object.keys(user).length) return;
try {
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [user])
note: fwiw, at first sight your user state looks redundant since it's derived from a calculated value.

Categories