Writing tests with react and context api - reactjs

I'm trying to write a test and I'm not getting the result thats its intended, can someone see what I'm doing wrong here please? I'm trying to call my addTodo but all I get is the error message below, quite new to testing so I'm not sure what it means.
my error message is this:
● <TodoForm /> #addTodo
expect(jest.fn()).toBeCalledWith(...expected)
Expected: {"payload": "a new todo", "type": "addTodo"}
Number of calls: 0
24 | form.find("button").simulate("click");
25 |
> 26 | expect(dispatch).toBeCalledWith({ type: "addTodo", payload: "a new todo" });
| ^
27 | });
28 |
at Object.<anonymous> (src/tests/TodoForm.test.js:26:20)
Here the relevant files:
the test: TodoForm.test.js
import React from "react";
import Enzyme, { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { Provider } from "../context/TodoContext";
import TodoForm from "../components/TodoForm";
Enzyme.configure({ adapter: new Adapter() });
test("<TodoForm /> #addTodo", async () => {
const dispatch = jest.fn();
const form = mount(
<Provider value={{ dispatch }}>
<TodoForm />
</Provider>
);
form.find("input").simulate("change", { target: { value: "a new todo" } });
form.find("button").simulate("click");
expect(dispatch).toBeCalledWith({ type: "addTodo", payload: "a new todo" });
});
the file I'm trying to write the test for:
import React, { useContext, useState } from "react";
import { Context } from "../context/TodoContext";
import "../css/TodoForm.css";
const TodoForm = ({ initialValues }) => {
const { addTodo } = useContext(Context);
const [title, setTitle] = useState("");
const [error, setError] = useState("");
function handleTodoAdd() {
if (title === "") {
setError(true);
} else {
setError(false);
}
setTitle("");
}
const onChange = e => {
setTitle(e.target.value);
};
/*
function handleSubmitForm(event) {
if (event.keyCode === 13) handleTodoAdd();
}
*/
return (
<div className="container">
<div className="inputContainer">
<div className="input-group">
<input
autoFocus={true}
aria-label="Enter the title of your todo"
placeholder="Enter new todo"
value={title}
onChange={onChange}
/>
<div className="errorContainer">
<span className="error">
{error ? "Please enter a value" : null}
</span>
</div>
<div className="input-group-append">
<button
aria-label="Add todo to your list"
className="addButton"
onClick={() => addTodo(title)}
>
Add
</button>
</div>
</div>
</div>
</div>
);
};
TodoForm.defaultProps = {
initialValues: {
title: "adsda"
}
};
export default TodoForm;
TodoContext.js I'm using this file to pull my provider
import React from "react";
import createDataContext from "./createDataContext";
export const TodoContext = React.createContext();
export default function todoReducer(state, action) {
switch (action.type) {
case "addTodo":
return [
...state,
{ id: Math.floor(Math.random() * 999), title: action.payload }
];
case "deleteTodo":
return state.filter(todo => todo.id !== action.payload);
case "editTodo":
return state.map(todo => {
return todo.id === action.payload.id ? action.payload : todo;
});
default:
return state;
}
}
const addTodo = dispatch => {
return title => {
dispatch({ type: "addTodo", payload: title });
};
};
const deleteTodo = dispatch => {
return id => {
dispatch({ type: "deleteTodo", payload: id });
};
};
const editTodo = dispatch => {
return (id, title) => {
dispatch({ type: "editTodo", payload: { id, title } });
};
};
export const { Context, Provider } = createDataContext(
todoReducer,
{ addTodo, deleteTodo, editTodo },
[]
);
auto creates my context data
import React, { useReducer } from "react";
export default (reducer, actions, initialState) => {
const Context = React.createContext();
const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const boundActions = {};
for (let key in actions) {
boundActions[key] = actions[key](dispatch);
}
return (
<Context.Provider value={{ state, ...boundActions }}>
{children}
</Context.Provider>
);
};
return { Context, Provider };
};

Related

TypeError: useContext(...) is undefined

I'm trying to use a custom hook that bring me functions to handle my TODOS on my context, but it gives me an error
Uncaught TypeError: useContext(...) is undefined
The above error occurred in the component:
Complete Error Image
TodoProvider.jsx
import { useReducer } from 'react';
import { useTodos } from '../hooks/useTodos';
import { TodoContext, todoReducer } from './';
export const TodoProvider = ({ children }) => {
const init = () => {
return [];
};
const [todos, dispatchTodos] = useReducer(todoReducer, {}, init);
const { handleNewTodo, handleToggleTodo } = useTodos();
return (
<TodoContext.Provider
value={{ todos, dispatchTodos, handleNewTodo, handleToggleTodo }}
>
{children}
</TodoContext.Provider>
);
};
useTodos.js
import { useContext } from 'react';
import { TodoContext } from '../context';
import { types } from '../types/types';
export const useTodos = () => {
const { dispatchTodos } = useContext(TodoContext);
const handleNewTodo = todo => {
const action = {
type: types.add,
payload: todo,
};
dispatchTodos(action);
};
const handleToggleTodo = id => {
dispatchTodos({
type: types.toggle,
payload: id,
});
};
return { handleNewTodo, handleToggleTodo };
};
The error traceback in your image says
`useContext(...)` is not defined
useTodos (useTodos.js:6)
Since you aren't showing your useTodos.js file, I must rely on my crystal ball to tell me that you've forgotten to
import {useContext} from 'react';
in useTodos.js, hence "not defined".
Here's an one-file example based on your code that verifiably does work...
import { useReducer, useContext, createContext } from "react";
function todoReducer(state, action) {
switch (action.type) {
case "add":
return [...state, { id: +new Date(), text: action.payload }];
default:
return state;
}
}
const TodoContext = createContext([]);
const TodoProvider = ({ children }) => {
const [todos, dispatchTodos] = useReducer(todoReducer, null, () => []);
return (
<TodoContext.Provider value={{ todos, dispatchTodos }}>
{children}
</TodoContext.Provider>
);
};
function useTodoActions() {
const { dispatchTodos } = useContext(TodoContext);
function handleNewTodo(todo) {
dispatchTodos({
type: "add",
payload: todo
});
}
function handleToggleTodo(id) {
dispatchTodos({
type: "toggle",
payload: id
});
}
return { handleNewTodo, handleToggleTodo };
}
function useTodos() {
return useContext(TodoContext).todos;
}
function TodoApp() {
const todos = useTodos();
const { handleNewTodo } = useTodoActions();
return (
<div>
{JSON.stringify(todos)}
<hr />
<button onClick={() => handleNewTodo((+new Date()).toString(36))}>
Add todo
</button>
</div>
);
}
export default function App() {
return (
<TodoProvider>
<TodoApp />
</TodoProvider>
);
}

redux persist not storing line items(cart element) data on refresh

I am find problem in persisting data when page is refreshed. I've a file with reducer and actions. I'm using shopify-js-buy sdk for cart of my project. The cart works fine if there is not page refresh but when there is a page refresh cart item data is not persisting. I feel that if I use only open/close state is persisted but other data like subtotal , items, no.of items etc. are not persisting.
this is reducer and action file:
import { useSelector, useDispatch } from "react-redux"
import Client from "shopify-buy"
// Creates the client with Shopify-Buy and store info
//Example Storefront
const client = Client.buildClient({
storefrontAccessToken: "dd4d4dc146542ba7763305d71d1b3d38",
domain: "graphql.myshopify.com",
})
const PRODUCTS_FOUND = "shopify/PRODUCTS_FOUND"
const PRODUCT_FOUND = "shopify/PRODUCT_FOUND"
const COLLECTION_FOUND = "shopify/COLLECTION_FOUND"
const CHECKOUT_FOUND = "shopify/CHECKOUT_FOUND"
const SHOP_FOUND = "shopify/SHOP_FOUND"
const ADD_VARIANT_TO_CART = "shopify/ADD_VARIANT_TO_CART"
const UPDATE_QUANTITY_IN_CART = "shopify/UPDATE_QUANTITY_IN_CART"
const REMOVE_LINE_ITEM_IN_CART = "shopify/REMOVE_LINE_ITEM_IN_CART"
const OPEN_CART = "shopify/OPEN_CART"
const CLOSE_CART = "shopify/CLOSE_CART"
const CART_COUNT = "shopify/CART_COUNT"
const initialState = {
isCartOpen: false,
cartCount: 0,
checkout: {},
products: [],
featured: [],
product: {},
shop: {},
}
export default (state = initialState, action) => {
switch (action.type) {
case PRODUCTS_FOUND:
return { ...state, products: action.payload }
case PRODUCT_FOUND:
return { ...state, product: action.payload }
case COLLECTION_FOUND:
return { ...state, featured: action.payload }
case CHECKOUT_FOUND:
return { ...state, checkout: action.payload }
case SHOP_FOUND:
return { ...state, shop: action.payload }
case ADD_VARIANT_TO_CART:
return { ...state, checkout: action.payload }
case UPDATE_QUANTITY_IN_CART:
return { ...state, checkout: action.payload }
case REMOVE_LINE_ITEM_IN_CART:
return { ...state, checkout: action.payload }
case OPEN_CART:
return { ...state, isCartOpen: true }
case CLOSE_CART:
return { ...state, isCartOpen: false }
case CART_COUNT:
return { ...state, cartCount: action.payload }
default:
return state
}
}
// Gets all the products from Shopify
export function getProducts() {
return (dispatch) => {
client.product.fetchAll().then((resp) => {
dispatch({
type: PRODUCTS_FOUND,
payload: resp,
})
})
}
}
// Gets individual item based on id
export function getProduct(id) {
return async (dispatch) => {
const resp = await client.product.fetch(id)
dispatch({
type: PRODUCT_FOUND,
payload: resp,
})
return resp
}
}
// Creates initial checkout state from Shopify
export function checkout() {
return (dispatch) => {
client.checkout.create().then((resp) => {
dispatch({
type: CHECKOUT_FOUND,
payload: resp,
})
})
}
}
// Gets Shopify store information
export function shopInfo() {
return (dispatch) => {
client.shop.fetchInfo().then((resp) => {
dispatch({
type: SHOP_FOUND,
payload: resp,
})
})
}
}
// Adds variants to cart/checkout
export function addVariantToCart(checkoutId, lineItemsToAdd) {
return async (dispatch) => {
const response = await client.checkout.addLineItems(
checkoutId,
lineItemsToAdd
)
dispatch({
type: ADD_VARIANT_TO_CART,
payload: response,
})
return response
}
}
// Updates quantity of line items in cart and in checkout state
export function updateQuantityInCart(lineItemId, quantity, checkoutId) {
const lineItemsToUpdate = [
{ id: lineItemId, quantity: parseInt(quantity, 10) },
]
return async (dispatch) => {
const resp = await client.checkout.updateLineItems(
checkoutId,
lineItemsToUpdate
)
dispatch({
type: UPDATE_QUANTITY_IN_CART,
payload: resp,
})
return resp
}
}
// Removes line item from cart and checkout state
export function removeLineItemInCart(checkoutId, lineItemId) {
return (dispatch) => {
client.checkout.removeLineItems(checkoutId, [lineItemId]).then((resp) => {
dispatch({
type: REMOVE_LINE_ITEM_IN_CART,
payload: resp,
})
})
}
}
// To close the cart
export function handleCartClose() {
return {
type: CLOSE_CART,
}
}
// To open the cart
export function handleCartOpen() {
return {
type: OPEN_CART,
}
}
// Set the count of items in the cart
export function handleSetCount(count) {
return {
type: CART_COUNT,
payload: count,
}
}
// this is exporting reducer file as shopifyState
export { default as shopifyState } from "./Shopify" //this written in another file.
//this store.js
//===================================================================================
import { createStore, combineReducers, applyMiddleware, compose } from "redux"
import thunk from "redux-thunk";
import * as reducers from "../ReduxStore/ShopifyCart";
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const rootReducer = combineReducers(reducers)
const enhancer = composeEnhancers(applyMiddleware(thunk))
const persistConfig = {
key: 'root',
storage,
stateReconciler: autoMergeLevel2,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const Store = createStore(persistedReducer, undefined, enhancer)
export const PersistStore = persistStore(Store);
//this is index.js
ReactDOM.render(
<BrowserRouter>
<Provider store={Store}>
<PersistGate loading={null} persistor={PersistStore}>
<React.StrictMode>
<App />
</React.StrictMode>
</PersistGate>
</Provider>
</BrowserRouter>,
document.getElementById('root')
);
//this file contains code for showing specific product based on input from another file
import React, { useEffect, useState } from "react"
import { useDispatch, useSelector } from 'react-redux';
import { Link } from "react-router-dom"
// import { useShopify } from "../../../../hooks"
import {addVariantToCart,getProduct,handleCartOpen} from '../../../../ReduxStore/ShopifyCart/Shopify'
export default (props) => {
const dispatch = useDispatch()
const checkoutState = useSelector(
(state) => state.shopifyState.checkout
)
const product = useSelector(
(state)=>(state.shopifyState.product)
)
const id = props.match.params.productId
const defaultSize = product.variants && product.variants[0].id.toString()
const [size, setSize] = useState("")
const [quantity, setQuantity] = useState(1)
const description = product.description && product.description.split(".")
function changeSize(sizeId, quantity) {
dispatch(handleCartOpen())
if (sizeId === "") {
sizeId = defaultSize
const lineItemsToAdd = [
{ variantId: sizeId, quantity: parseInt(quantity, 10) },
]
const checkoutId = checkoutState.id
// addVariant(checkoutId, lineItemsToAdd)
dispatch(addVariantToCart(checkoutId, lineItemsToAdd))
} else {
const lineItemsToAdd = [
{ variantId: sizeId, quantity: parseInt(quantity, 10) },
]
const checkoutId = checkoutState.id
// addVariant(checkoutId, lineItemsToAdd)
dispatch(addVariantToCart(checkoutId, lineItemsToAdd))
}
}
useEffect(() => {
(dispatch(getProduct(id)))
}, [id])
return (
<div id="individualProduct">
<Link className="homeButton button" to={"/Home"}>
Home
</Link>
<div className="Product-wrapper2">
<div className="Images">
{product.images &&
product.images.map((image, i) => {
return (
<img
key={image.id + i}
src={image.src}
alt={`${product.title} product shot`}
/>
)
})}
</div>
<div className="Product__info">
<h2 className="Product__title2">{product.title}</h2>
<ul className="Product__description">
{description &&
description.map((each, i) => {
return <li key={`line-description +${i}`}>{each}</li>
})}
</ul>
<div>
<label htmlFor={"prodOptions"}>Size</label>
<select
id="prodOptions"
name={size}
onChange={(e) => {
setSize(e.target.value)
}}
>
{product.variants &&
product.variants.map((item, i) => {
return (
<option
value={item.id.toString()}
key={item.title + i}
>{`${item.title}`}</option>
)
})}
</select>
</div>
<div>
<label>Quantity</label>
<input
className="quantity"
type="number"
min={1}
value={quantity}
onChange={(e) => {
setQuantity(e.target.value)
}}
></input>
</div>
<h3 className="Product__price">
${product.variants && product.variants[0].price}
</h3>
<button
className="prodBuy button"
onClick={(e) => changeSize(size, quantity)}
>
Add to Cart
</button>
</div>
</div>
</div>
)
}
//this is cart code
import React, { useEffect } from "react"
import LineItem from "../LineItem/LineItem"
// import { useShopify } from "../../../../hooks"
import { useSelector, useDispatch } from "react-redux"
import {handleCartOpen,handleCartClose,handleSetCount} from '../../../../ReduxStore/ShopifyCart/Shopify'
// import { MdShoppingCart, MdRemoveShoppingCart } from "react-icons/md"
export default (props) => {
const cartStatus = useSelector((appState) => appState.shopifyState.isCartOpen)
const checkoutState = useSelector(
(appState) => appState.shopifyState.checkout
)
const dispatch = useDispatch()
function handleOpen(e) {
e.preventDefault()
dispatch(handleCartOpen())
}
function handleClose(e) {
e.preventDefault()
dispatch(handleCartClose())
}
function openCheckout(e) {
e.preventDefault()
// window.open(checkoutState.webUrl) // opens checkout in a new window
window.location.replace(checkoutState.webUrl) // opens checkout in same window
}
useEffect(() => {
const button = document.querySelector("button.App__view-cart")
if (cartStatus === true) {
button.classList.add("hide")
} else {
button.classList.remove("hide")
}
function getCount() {
let lineItems =
checkoutState.lineItems && checkoutState.lineItems.length > 0
? checkoutState.lineItems
: []
let count = 0
lineItems.forEach((item) => {
count += item.quantity
return count
})
dispatch(handleSetCount(count))
}
getCount()
}, [cartStatus, checkoutState])
return (
<div id="cart">
<div className={`Cart ${cartStatus ? "Cart--open" : ""}`}>
<div className="App__view-cart-wrapper2">
<button className="App__view-cart" onClick={(e) => handleOpen(e)}>
{/* <MdShoppingCart /> */}
</button>
</div>
<header className="Cart__header">
<h2>Your cart</h2>
<button className="Cart__close" onClick={(e) => handleClose(e)}>
{/* <MdRemoveShoppingCart /> */}
<span style={{fontSize: '30px'}}>X</span>
</button>
</header>
<ul className="Cart__line-items">
<LineItem />
</ul>
<footer className="Cart__footer">
<div className="Cart-info clearfix">
<div className="Cart-info__total Cart-info__small">Subtotal</div>
<div className="Cart-info__pricing">
<span className="pricing">$ {checkoutState.subtotalPrice}</span>
</div>
</div>
<div className="Cart-info clearfix">
<div className="Cart-info__total Cart-info__small">Taxes</div>
<div className="Cart-info__pricing">
<span className="pricing">$ {checkoutState.totalTax}</span>
</div>
</div>
<div className="Cart-info clearfix">
<div className="Cart-info__total Cart-info__small">Total</div>
<div className="Cart-info__pricing">
<span className="pricing">$ {checkoutState.totalPrice}</span>
</div>
</div>
<button
className="Cart__checkout button"
onClick={(e) => openCheckout(e)}
>
Checkout
</button>
</footer>
</div>
</div>
)
}
I'm working on reactJs. Will be thankfull for any help on this .

useSelector hooks problem after submitting data

I'm not sure if the problem is in useSelector or in useDispatch hooks or in another place, so here is the scenario:
Two screens (HomeScreen & AddBlogScreen)
In HomeScreen I click add blog button then it redirect to AddBlogScreen
I input the data, then submit. After the submit is success then redirect to HomeScreen
As mentioned in below pic, I got the no 4 result & I have to refresh to get the no 3 result. But my expectation is no 3 pic without getting the error.
Here is my code:
HomeScreen
import jwtDecode from "jwt-decode";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useHistory } from "react-router";
import { blogList } from "../redux/action";
export const MainScreen = () => {
// const [token, setToken] = useState(localStorage.getItem("token"));
const user = jwtDecode(localStorage.getItem("token"));
const history = useHistory();
const dispatch = useDispatch();
useEffect(() => {
dispatch(blogList());
}, [dispatch]);
const { blog } = useSelector((state) => state.blog);
console.log(blog);
return (
<>
<button
onClick={() => {
localStorage.removeItem("token");
history.push("/");
}}
>
singout
</button>
<button
onClick={() => {
history.push({ pathname: "/Blog", state: user });
}}
>
add blog
</button>
<h1 style={{ color: "red" }}>username: {user.username}</h1>
{blog.map(({ id, b_title, b_content, category_id }) => (
<div key={id}>
<h1
onClick={() =>
history.push({
pathname: "/Edit",
state: { id, b_title, b_content, category_id },
})
}
>
Title: {b_title}
</h1>
<p>Content: {b_content}</p>
</div>
))}
</>
);
};
AddBlogScreen
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { addBlog } from "../redux/action";
export const AddBlogScreen = () => {
const history = useHistory();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [category, setCategory] = useState("");
const dispatch = useDispatch();
const location = useLocation();
const author = location.state.id;
const submitHandler = (e) => {
e.preventDefault();
dispatch(addBlog(title, content, author, category));
setTitle("");
setContent("");
setCategory("");
history.push("/Home");
};
return (
<div>
<h1>add blog page</h1>
<form onSubmit={submitHandler}>
<input
type="text"
placeholder="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<br />
<br />
<input
type="text"
placeholder="content"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<br />
<br />
<input
type="text"
placeholder="category"
value={category}
onChange={(e) => setCategory(e.target.value)}
/>
<br />
<br />
<input
type="submit"
value="submit"
disabled={
title === "" || content === "" || category === "" ? true : false
}
/>
</form>
</div>
);
};
actions
import axios from "axios";
import {
LIST_BLOG,
ADD_BLOG,
EDIT_BLOG,
DELETE_BLOG,
LOGIN_USER,
REGISTER_USER,
LOGOUT_USER,
} from "./constant";
// ==================== blog actions ======================
export const blogList = () => async (dispatch) => {
try {
const result = await axios
.get("http://localhost:3001/api/v1/blog?page=0")
.then((res) => res.data.data)
.catch((err) => err);
dispatch({
type: LIST_BLOG,
payload: result,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
export const addBlog =
(title, content, author, category) => async (dispatch) => {
try {
const result = await axios
.post("http://localhost:3001/api/v1/blog", {
blog_title: title,
blog_content: content,
author_id: author,
category_id: category,
})
.then(alert("success add blog"))
.catch((err) => alert(err));
dispatch({
type: ADD_BLOG,
payload: result,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
reducer
const initial_state = {
blog: [],
};
export const blogReducer = (state = initial_state, action) => {
switch (action.type) {
case LIST_BLOG:
return {
...state,
blog: action.payload,
};
case ADD_BLOG:
return {
...state,
blog: action.payload,
};
case EDIT_BLOG:
return {
...state,
blog: action.payload,
};
case DELETE_BLOG:
return {
...state,
blog: action.payload,
};
default:
return state;
}
};
store
import { blogReducer, userReducer } from "./reducer";
import { combineReducers, createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
const reducer = combineReducers({
blog: blogReducer,
user: userReducer,
});
const middleWare = composeWithDevTools(applyMiddleware(thunk));
export const store = createStore(reducer, middleWare);
First of all, the origin of error:
the error says a property named map on blog is not a function, meaning blog is not an array.
This is where it is coming from:
const { blog } = useSelector((state) => state.blog);
Your state is a an ojbect with a property named blog, you can access it these two ways:
const { blog } = useSelector((state) => state);
or
const blog = useSelector((state) => state.blog);
Other issues I noticed :
in addBlog:
1. When you are using try-catch with await, it's not a good idea to use then-catch too.
2.result won't be the blog data you expect. It will be an object, which is an instance of AxiosResponse, which includes the data.
you can extract the data from response object this way:
let response = await axios.post(... // some api request
let {data}=response
I would edit it like this:
export const addBlog =
(title, content, author, category) => async (dispatch) => {
try {
const {data} = await axios
.post("http://localhost:3001/api/v1/blog", {
blog_title: title,
blog_content: content,
author_id: author,
category_id: category,
})
alert("success add blog")
dispatch({
type: ADD_BLOG,
payload: data,
});
} catch (err) {
dispatch({
payload: err,
});
}
};
I found the solution, so in my action I changed it into:
dispatch({
type: LIST_BLOG,
payload: result.data.data,
});

Add new property to state object from a form component with redux

I have a form that adds new articles. I need to create another form that triggers when I click on a created article and add a property "keyword" to the article state and display it. I tried to do something but I am kinda stuck.
Form.jsx component that adds the article/s:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { v1 as uuidv1 } from 'uuid';
import { ADD_ARTICLE } from '../constants/action-types';
const Form = () => {
const [title, setTitle] = useState('');
const dispatch = useDispatch();
const handleChange = (e) => {
const { value } = e.target
setTitle(value);
}
const handleSubmit = (e) => {
e.preventDefault();
const id = uuidv1();
dispatch({ type: ADD_ARTICLE, payload: { id, title } });
setTitle('');
}
return (
<form onSubmit={handleSubmit}>
<div className='form-group'>
<label htmlFor='title'>Title</label>
<input
type='text'
className='form-control'
id='title'
value={title}
onChange={handleChange}
/>
</div>
<input className='btn btn-success btn-lg' type='submit' value='SAVE' />
</form>
);
}
export default Form;
List.jsx component where the articles are displayed:
import React, { useState,useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import KeywordForm from './KeywordForm.jsx';
import { fetchArticles } from '../thunk';
const List = () => {
const [showForm,setShowForm]=useState(false);
const articles = useSelector(state => state.articles);
const dispatch = useDispatch();
const displayForm=()=>{
setShowForm(!showForm)
}
useEffect(() => {
dispatch(fetchArticles);
}, []);
return (
<>
<ul className='list-group list-group-flush'>
{articles.map(article => (
<li className='list-group-item' key={article.id} onClick={displayForm}>
{article.title}
</li>
))}
</ul>
<div>
{showForm && (
<KeywordForm />
)}
</div>
</>
);
}
export default List;
Here i added a state that displays the KeywordForm component when I click an article.
KeywordForm.jsx component,this is the one that I created to add the keyword:
import React, { useState } from 'react';
import { useDispatch ,useSelector} from 'react-redux';
import { ADD_KEYWORD } from '../constants/action-types';
const KeywordForm = ({id,title}) => {
const [keyword,setKeyword]=useState('');
const articles = useSelector(state => state.articles);
const dispatch=useDispatch();
console.log(articles)
const handleChange = (e) => {
const { value } = e.target
setKeyword(value);
}
const handleSubmit = (e) => {
e.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<div className='form-group'>
<label htmlFor='keyword'>Keyword</label>
<input
type='text'
className='form-control'
id='keyword'
value={keyword}
onChange={handleChange}
/>
</div>
<input className='btn btn-success btn-lg' type='submit' value='SAVE' />
</form>
);
}
export default KeywordForm;
reducers.js
const initialState = {
articles: []
};
const rootReducer = (state = initialState, action) => {
const { type, payload } = action;
switch(type) {
case ADD_ARTICLE: {
return {...state,
articles: [...state.articles,payload]
};
}
case ADD_KEYWORD: {
return Object.assign({}, state, {
articles: state.articles.concat(payload)
});
}
case ARTICLES_RETRIEVED: {
return Object.assign({}, state, {
articles: state.articles.concat(payload)
});
}
default:
return state;
}
}
export default rootReducer;
actions.js
import { ADD_ARTICLE, ARTICLES_RETRIEVED,ADD_KEYWORD } from '../constants/action-types';
const addArticle = (payload) => {
return { type: ADD_ARTICLE, payload };
}
const addKeyword = (payload) => {
return { type: ADD_KEYWORD, payload };
}
const articlesRetrieved = (payload) => {
return { type: ARTICLES_RETRIEVED, payload };
}
export { addArticle, articlesRetrieved,addKeyword };
What should i add to my reducers/actions to make this work? My idea is that i have to somehow pass the id of the article clicked and then in the reducer find it's index or something and check it with the payload.id .
You want to modify an existing article in the state and a keyword to it (can there be an array of keywords, or just one?). In order to do that, your action payload will need to contain both the keyword and the id of the article that it belongs to.
Your reducer will find the article that matches the id and replace it with a copied version that has the keyword added to it.
case ADD_KEYWORD: {
return {
...state,
articles: state.articles.map(article =>
// find the article to update
article.id === payload.id ?
// update it
{ ...article, keyword: payload.keyword } :
// otherwise return the original
article
}
}
This is easier to do with the official Redux Toolkit because you can modify the draft state directly and you don't need to worry about mutations.

POST request using React with Hooks and Redux

I am making a React app where I need to add Redux using Hooks. Currently, I am stuck in making a POST request and can't figure out after going through the internet, how to make it work. I am on my way to understand how the Redux works and I will be happy for any help on this to make it work, so I can understand what is missing and how to send the data. My components:
App.js:
import { useState } from "react";
import { connect } from 'react-redux';
import "./App.css";
import Posts from "./components/posts";
import { addPost } from "./store/actions/postAction";
function App() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
const post = {
title: title,
body: body,
}
addPost(post);
setTitle('');
setBody('');
alert("Post added!");
};
return (
<div className="App">
<Posts />
<form onSubmit={handleSubmit}>
<label>
Mew post:
<input
type="text"
name="title"
placeholder="Add title"
value={title}
onChange={e => setTitle(e.target.value)}
/>
<input
type="text"
name="body"
placeholder="Add body"
value={body}
onChange={e => setBody(e.target.value)}
/>
</label>
<button type="submit">Add</button>
</form>
</div>
);
}
export default connect()(App);
postAction.js
import axios from "axios";
import { GET_POSTS, ADD_POST, POSTS_ERROR } from "../types";
const url = "http://localhost:8002/";
export const getPosts = () => async (dispatch) => {
try {
const response = await axios.get(`${url}posts`);
dispatch({
type: GET_POSTS,
payload: response.data,
});
} catch (error) {
dispatch({
type: POSTS_ERROR,
payload: error,
});
}
};
export const addPost = (post) => (dispatch) => {
try {
const response = axios.post(`${url}`, {post});
dispatch({
type: ADD_POST,
payload: response.data,
});
} catch (error) {
dispatch({
type: POSTS_ERROR,
payload: error,
});
}
};
postReducer.js
import { ADD_POST, GET_POSTS, POSTS_ERROR } from "../types";
const initialState = {
posts: []
};
const postReducer = (state = initialState, action) => {
switch (action.type) {
case GET_POSTS:
return {
...state,
posts: action.payload
};
case ADD_POST:
return {
...state,
posts: action.payload
};
case POSTS_ERROR:
return {
error: action.payload
};
default:
return state;
}
};
export default postReducer;
posts.js
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../store/actions/postAction";
const Posts = () => {
const dispatch = useDispatch();
const postsList = useSelector((state) => state.postsList);
const { loading, error, posts } = postsList;
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
return (
<>
{loading
? "Loading..."
: error
? error.message
: posts.map((post) => (
<div className="post" key={post.id}>
<h4>{post.title}</h4>
<p>{post.body}</p>
</div>
))}
</>
);
};
export default Posts;
App.js -> change to export default connect(null, {addPost})(App);

Resources