I'm updating an array and I wanted to update the productCode based on the given newCode response. This is by clicking the 'CREATE ALL PRODUCTS' button.
I'm thinking that the problem is on the reducer. It's currently not updating the productCode and newProductCode
Tip: productIndex is the key to finding it
Click Here: CODESANDBOX
Action
export const createAllProducts = (products) => async (dispatch) => {
try {
dispatch({
type: appConstants.CREATE_ALL_PRODUCTS_REQUEST
});
const responses = [
{
config: null,
data: {
newCode: "NEW_AA"
},
headers: null
},
{
config: null,
data: {
newCode: "NEW_FF"
},
headers: null
},
{
config: null,
data: {
newCode: "NEW_GG"
},
headers: null
}
];
const finalResponses = responses.map((product, index) => ({
newProductCode: product.data.newCode,
productCode: product.data.newCode,
productIndex: products[index].productIndex
}));
console.log(finalResponses);
dispatch({
type: appConstants.CREATE_ALL_PRODUCTS_SUCCESS,
payload: finalResponses
});
} catch (error) {
dispatch({
type: appConstants.CREATE_ALL_PRODUCTS_FAILURE
});
}
};
Reducer
case appConstants.CREATE_ALL_PRODUCTS_SUCCESS:
const updatedProducts = state.products.map((product, index) => {
const found = action.payload.find((el) => el.productIndex === index);
return found
? {
...updatedProducts,
productCode: found.productCode,
newProductCode: found.newProductCode
}
: product;
});
return {
...state,
isCreatingAllProducts: false,
products: updatedProducts
};
The issue is with the reducer
case appConstants.CREATE_ALL_PRODUCTS_SUCCESS:
return {
...state,
products: state.products.map((product, index) => {
const found = action.payload.find((el) => el.productIndex === index);
console.log(found);
return found
? {
...product,
productCode: found.productCode,
newProductCode: found.newProductCode
}
: product;
})
};
You used reduce methods with the initial value state, which is the actually old state.
Consider this example:
const state = { history: null }
const payload = [ 'hello', 'equal' ]
//your current reducer
const newState = payload.reduce((acc, cur) => { acc[cur] = cur; return acc } , state)
//the state reference point to the same obj, then redux will not trigger re-render
console.log(newState === state) // true
Related
I am trying to reload my table of data once my useMutation has completed.
On page load i am querying:
const { loading: appLoading, data: applicationsData } = useQuery(
applications.operations.GET_APPLICATIONS_BY_COMPANY,
{
client: applications.client,
variables: { companyId: userDetails.companyId },
}
)
when a user selects a button to clone a record:
const [
CloneApplication,
{ loading: cloneLoading, data: cloneData, error: cloneError },
] = useMutation(applications.operations.CLONE_APPLICATION_BY_COMPANY, {
client: applications.client,
onCompleted: (data) => {
setFinalData((prev) => [...prev, data]), console.log('data', data)
},
})
im adding a record to the list but when i refresh its not there. My assumption is instead of adding it to state, I need to refetch the applications and then save that to state which in turn will automatically refresh the table.
My question is how can i do that?
Edit:
const { applications } = apis
const { queryString, parameters } = getTemplatesListApiDetails()
const [finalData, setFinalData] = useState<any>([])
const [templatesList, setTemplatesList] = useState([])
const { loading, data } = useQuery(queryString, parameters)
const { loading: appLoading, data: applicationsData } = useQuery(
applications.operations.GET_APPLICATIONS_BY_COMPANY,
{
client: applications.client,
variables: { companyId: userDetails.companyId },
}
)
const [
CloneApplication,
{ loading: cloneLoading, data: cloneData, error: cloneError },
] = useMutation(applications.operations.CLONE_APPLICATION_BY_COMPANY, {
client: applications.client,
refetchQueries: [
{ query: applications.operations.GET_APPLICATIONS_BY_COMPANY },
],
})
useEffect(() => {
if (data && data.getCompanyTemplates)
setTemplatesList(
userDetails.globalTemplates === false
? data.getCompanyTemplates
: data.getAllTemplates
)
if (applicationsData && templatesList) {
const newFinalData = getFinalData({
applicationsList: applicationsData.getApplicationsByCompany,
templatesList: templatesList,
})
setFinalData(newFinalData)
}
}, [applicationsData, cloneData, data, templatesList])
getFinalData Function
export function getFinalData(request: {
templatesList: GetAllTemplate[]
applicationsList: GetApplicationsByCompany[]
}): FinalDataResponse[] {
const templates = request.templatesList.map((template) => {
const applicationsForTemplate = request.applicationsList.filter(
(app) => app.templateId === template.templateId
)
return { ...template, applications: applicationsForTemplate }
})
const groupedData = _.chain(templates)
.groupBy('templateId')
.map((value, key) => {
const templateName = _.chain(value)
.groupBy('templateName')
.map((value, key) => key)
.value()
const createdDate = _.chain(value)
.groupBy('dateCreated')
.map((value, key) => dayjs(key).format('ll'))
.value()
const lastModified = _.chain(value)
.groupBy('lastModified')
.map((value, key) => dayjs(key).format('ll'))
.value()
return {
templateId: key,
templateName: templateName[0],
createdDate: createdDate[0],
lastModified: lastModified[0],
applications: value[0].applications,
}
})
.value()
const finalData = groupedData.map((object, index) => {
return {
...object,
totalApplications: object.applications.length,
}
})
console.log('returning final data: ', finalData)
return finalData
}
To refetch the data automatically, you need to invalidate the previously cached results. In apollo, this is done using refetchQueries:
useMutation(applications.operations.CLONE_APPLICATION_BY_COMPANY, {
refetchQueries: [{ query: applications.operations.CLONE_APPLICATION_BY_COMPANY}]
})
More ways of solving this here: https://www.apollographql.com/blog/apollo-client/caching/when-to-use-refetch-queries/
i'm migrating slowly to redux-toolkit
when i try to delete a single item from my store, the action works well because i send the right ID from the component, but i think that the way i send this id to the slice is incorrect so i'm not able to delete the right item from my store(it delete all the items)
component dispatch code:
const handleDelete = async (e) => {
e.preventDefault()
const del_id = e.currentTarget.getAttribute("data-id")
dispatch(deleteComment(del_id))
}
action & api call:
export const deleteComment = (id) => async (dispatch, getState) => {
try {
dispatch(deleteCommentsLoading())
const {
userLogin: { userInfo },
} = getState()
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`,
},
}
const { data } = await axios.delete(
`${process.env.REACT_APP_API_KEY}/publication/comment/delete/${id}`,
config
)
dispatch(deleteCommentsSuccess({ commentId: id }))
// i tried deleteCommentsSuccess(data); (id)... nothing work to send this id to the slice
} catch (error) {
dispatch(
deleteCommentsFail(
error.response && error.response.data.message
? error.response.data.message
: error.message
)
)
}
}
my slice:
const commentSlice = createSlice({
name: "comments",
initialState: {
comment: {},
loading: false,
error: false,
},
... other reducers
// DELETE COMMENT
deleteCommentsLoading: (state) => {
state.loading = true
},
deleteCommentsFail: (state, action) => {
state.error = action.payload
state.loading = false
},
deleteCommentsSuccess: (state, action) => {
const { commentId } = action.payload
state.comment.comments.filter((item) => item._id !== commentId)
// i tried first when i send data or id to put action.payload.id nothing work
state.loading = false
state.error = false
},
and this's my comments store slice, every item have his own "_id"
How is your backend. (comment)
Something like this? :
const receita = await Recipe.findById(req.params.id)
if (receita) {
await receita.remove()
res.json({ message: 'Receita removed' })
} else {
res.status(404)
throw new Error('Receita not found')
}
})
state.comment.comments.filter((item) => item._id !== commentId) will just return a filtered copy of the array,but not actually modify anything.
You have to do
state.comment.comments = state.comment.comments.filter((item) => item._id !== commentId)
I'm new to Firebase Realtime Database, and i'm trying to implement a search field that allow users to search for other users and view their profiles.
The Problem Is:
I want to make the search realTime(on each input change).but whenever a new request's sent, the old request is still working in the backend which's causing unexpected behavior,i've wrapped this functionality in a useEffect Hook,old sideEffects has to be cleaned up to make the query results predictable,how can i abort the previous request.
useSearchOwner Custom Hook:
const useSearchOwner = () => {
const [{ SearchValue, SearchResult, Search }, dispatch] = useReducer(
reducer,
{
SearchValue: "",
SearchResult: "",
Search: false,
}
);
const isFirstRender = useRef(true);
const onChangeHandler = (e) =>
dispatch({
type: ACTIONS.UPDATE_SEARCH_VALUE,
payload: { searchValue: e.target.value },
});
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
dispatch({ type: ACTIONS.START_SEARCHING });
const DispatchQueryByResult = async () => {
const ArrayOfOwners = await FirebaseUtilityInstance.SearchOwnerResult(
SearchValue
);
dispatch({
type: ACTIONS.UPDATE_SEARCH_RESULT,
payload: { searchResult: ArrayOfOwners },
});
dispatch({ type: ACTIONS.STOP_SEARCHING });
return () => {
FirebaseUtilityInstance.SearchOwnerCleanup();
};
};
DispatchQueryByResult();
}, [SearchValue]);
useEffect(() => {
console.log(SearchResult);
}, [SearchResult]);
return {
onChangeHandler: onChangeHandler,
Query: SearchValue,
QueryResult: SearchResult,
isSearching: Search,
};
};
Firebase Method To Do Query:
SearchOwnerResult = async (Query) => {
const { firstName, lastName } = getFirstNameAndLastName(Query);
let ArrayOfOwners = [];
await this.Database()
.ref("users")
.orderByChild("UserType")
.equalTo("owner")
.once("value", (snapshot) => {
const OwnersContainer = snapshot.val();
const keys = Object.keys(OwnersContainer);
for (let i = 0; i < keys.length; i++) {
const CurrentOwner = OwnersContainer[keys[i]];
if (
CurrentOwner.FirstName === firstName ||
CurrentOwner.LastName === lastName
) {
ArrayOfOwners.push(OwnersContainer[keys[i]]);
}
}
});
return ArrayOfOwners;
};
I am trying to get my head around Redux. Doing something like TODO APP with React and Redux. I can add a new task, update its value, but I cannot delete the item correctly. I get an error Unhandled Rejection (TypeError): Cannot read property 'id' of undefined all the time. I pass the ID to the Delete function just like I do in the Update function. Server side works well. The fetch function itself works, and the delete of item from the database works, but an error occurs on the client side Help please guys
const initialState = {
list: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case GET_TASKS: {
return { ...state, list: action.tasks }
}
case CREATE_TASK: {
return { ...state, list: [...state.list, action.task] }
}
case UPDATE_STATUS: {
return {
...state,
list: state.list.map((it) => {
return action.task.id === it.id ? action.task : it
}),
}
}
case DELETE_TASK: {
return {
list: state.list.map((it) => {
return action.task.id !== it.id ? action.task : it
}),
}
}
default:
return state
}
}
export function getTasks() {
return (dispatch) => {
fetch("/api/task")
.then((r) => r.json())
.then(({ data: tasks }) => {
dispatch({ type: GET_TASKS, tasks })
})
}
}
export function deleteTask(id) {
return (dispatch) => {
fetch(`/api/v1/task/${id}`, {
method: "DELETE",
})
.then((r) => r.json())
.then(({ data: task }) => {
dispatch({ type: DELETE_TASK, task })
})
}
}
My first question would be, in your deleteTask method what is being returned here? Does a delete method actually return the task you deleted?
.then(({ data: task }) => {
dispatch({ type: DELETE_TASK, task })
}
If not, another way you can address this is by changing the task in your dispatch to the id you are passing to the deleteTask method:
dispatch({ type: DELETE_TASK, id });
Then use the filter method instead of map in your reducer to return the tasks that don't match that deleted task's id, effectively "deleting" it from your state:
case DELETE_TASK: {
return {
list: state.list.filter((it) => {
return action.id !== it.id;
}),
}
}
I am using the useMemo hook return and filter an array of items. I then have a toggle function that toggles whether an item is true or false and then posts that item back to an API if it is true or false and adds it to a list. Within the function, which using the useReducer hook, the array is one step behind. For instance, the array of items gets returned and you toggle whether they are on sale or not, and if you toggle true they get added to the saleList and if they get toggled to not on sale they get added to notSaleList. In the function the saleList length will come back as 3 but it is really 4, then you remove a home to make it 3 but it will return 4. Does anybody know why that would be thanks?
const homesReducer = (state, action) => {
switch (action.type) {
case 'FETCH_INIT':
return {
...state,
isLoading: true,
isError: false,
};
case 'FETCH_SUCCESS':
//action.payload to object
const entities = action.payload.reduce((prev, next) => {
return { ...prev, [next.Id]: next };
}, {});
return {
...state,
isLoading: false,
isError: false,
homes: entities,
};
case 'FETCH_FAILURE':
return {
...state,
isLoading: false,
isError: true,
};
case 'TOGGLE_SALE_HOME_INIT':
return {
...state,
homes: {
...state.homes,
// ask Jenkins
[action.payload]: {
...state.homes[action.payload],
IsSaleHome: !state.homes[action.payload].IsSaleHome,
},
},
};
case 'TOGGLE_SALE_HOME_SUCCESS':
return {
...state,
};
case 'TOGGLE_SALE_HOME_FAILURE':
// TODO update back if failed
return {
...state,
homes: {
...state.homes,
// ask Jenkins
[action.payload]: {
...state.homes[action.payload],
IsSaleHome: !state.homes[action.payload].IsSaleHome,
},
},
};
default:
return { ...state };
}
};
const useOnDisplayApi = activeLotNumber => {
const [state, dispatch] = useReducer(homesReducer, {
isLoading: false,
isError: false,
homes: [],
saleHomes: [],
});
const homes = useMemo(() => {
return Object.keys(state.homes).map(id => {
return state.homes[id];
});
}, [state.homes]);
}
const saleHomes = useMemo(() => {
return homes.filter(home => {
return home.IsSaleHome;
});
}, [homes]);
const notSaleHomes = useMemo(() => {
return homes.filter(home => {
return !home.IsSaleHome && !home.IsSuggestedSaleHome;
});
}, [homes]);
const toggleSaleHome = async (home, undo = true) => {
dispatch({ type: 'TOGGLE_SALE_HOME_INIT', payload: home.Id });
try {
const didUpdate = await updateInventory(
activeLotNumber,
home.InventoryId,
{
InventoryId: home.InventoryId,
IsSaleHome: !home.IsSaleHome,
}
);
if (didUpdate == true) {
dispatch({ type: 'TOGGLE_SALE_HOME_SUCCESS' });
}
else {
dispatch({ type: 'TOGGLE_SALE_HOME_FAILURE', payload: home.Id });
}
} catch (error) {
setTimeout(() => {
dispatch({ type: 'TOGGLE_SALE_HOME_FAILURE' });
}, 600);
}
};
The update after dispatch isn't available immediately and is asynchronous. So your app will go through another render cycle to reflect the update.
You need to use useEffect to call the api after update and not call it on initial render.
const initialRender = useRef(true);
useEffect(() => {
if(initialRender.current) {
initialRender.current = false;
} else {
try {
const didUpdate = await updateInventory(
activeLotNumber,
home.InventoryId,
{
InventoryId: home.InventoryId,
IsSaleHome: !home.IsSaleHome,
}
);
if (didUpdate == true) {
dispatch({ type: 'TOGGLE_SALE_HOME_SUCCESS' });
}
else {
dispatch({ type: 'TOGGLE_SALE_HOME_FAILURE', payload: home.Id });
}
} catch (error) {
setTimeout(() => {
dispatch({ type: 'TOGGLE_SALE_HOME_FAILURE' });
}, 600);
}
}
}, [home])
const toggleSaleHome = async (home, undo = true) => {
dispatch({ type: 'TOGGLE_SALE_HOME_INIT', payload: home.Id });
}
I ended up fixing my issue by adding an if statement before the 'INIT'.
const toggleSaleHome = async (home, undo = true) => {
if (saleHomes.length > 9 && !home.IsSaleHome) {
toast.error(
<div>
{`${home.Name} could not be added. You already have selected 10 sale homes.`}
</div>,
{
className: 'error-toast',
progressClassName: 'error-progress-bar',
closeButton: false,
position: toast.POSITION.BOTTOM_RIGHT,
}
);
return;
}
dispatch({ type: 'TOGGLE_SALE_HOME_INIT', payload: home.Id });
try {
const didUpdate = await updateInventory(
activeLotNumber,
home.InventoryId,
{
InventoryId: home.InventoryId,
IsSaleHome: !home.IsSaleHome,
}
);
if (didUpdate == true) {
dispatch({ type: 'TOGGLE_SALE_HOME_SUCCESS' });
}
else {
dispatch({ type: 'TOGGLE_SALE_HOME_FAILURE', payload: home.Id });
}
} catch (error) {
setTimeout(() => {
dispatch({ type: 'TOGGLE_SALE_HOME_FAILURE' });
}, 600);
}
};
My whole issue was that I was not wanting any more homes to be able to be toggled once they reached 10 and before the 'INIT' the actual state of saleHomes is available, so the saleHomes.length is accurate.