My it's super simple but I get stuck.
I need to update an array on MongoDB with fetch PUT
I tested it with postman and works perfectly but my app React + Redux doesn't work
import React, { Fragment, useEffect, useState } from "react";
import PropTypes from "prop-types";
import "materialize-css/dist/css/materialize.min.css";
import M from "materialize-css/dist/js/materialize.min.js";
import config from "react-global-configuration";
import Preloader from "../layout/Preloader";
import { connect } from "react-redux";
import { getColors, updateColors } from "../../redux/actions/settingsActions";
const Settings = ({
setting: { settings, loading },
getColors,
updateColors
}) => {
const [HighPColor, setHighPColor] = useState("");
const [NormalPColor, setNormalPColor] = useState("");
const [LowPColor, setLowPColor] = useState("");
useEffect(() => {
M.AutoInit();
getColors();
//eslint-disable-next-line
}, []);
const onSubmit = () => {
const updColors = {
id: settings[0]._id,
colors: [
{
_id: colorsArray.colors[0]._id,
HighPColor,
NormalPColor,
LowPColor
}
]
};
updateColors(updColors);
M.toast({ html: "Settings updated" });
};
if (loading || settings === null) {
return <Preloader />;
}
const colorsArray = settings[0];
return (
<Fragment>
<div id="color-settings" className="container">
<div className="">
<h4>Set Priorities Colors </h4>
<div className="row">
<div>High Priority</div>
<div className="input-field">
<input
type="text"
name="highPColor"
defaultValue={colorsArray.colors[0].HighPColor}
onChange={e => setHighPColor(e.target.value)}
/>
</div>
</div>
<div className="row">
<div>Normal Priority</div>
<div className="input-field">
<input
type="text"
name="normalPColor"
defaultValue={colorsArray.colors[0].NormalPColor}
onChange={e => setNormalPColor(e.target.value)}
/>
</div>
</div>
<div className="row">
<div>Low Priority</div>
<div className="input-field">
<input
type="text"
name="lowPColor"
defaultValue={colorsArray.colors[0].LowPColor}
onChange={e => setLowPColor(e.target.value)}
/>
</div>
</div>
</div>
<div className="">
<a
href="#!"
onClick={onSubmit}
className="modal-close waves-effect blue btn"
>
Enter
</a>
</div>
</div>
</Fragment>
);
};
Settings.propTypes = {
setting: PropTypes.object.isRequired,
getColors: PropTypes.func.isRequired,
updateColors: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
setting: state.settings
});
export default connect(mapStateToProps, { getColors, updateColors })(Settings);
I take everything from some inputs values that work perfectly
Redux action:
export const updateColors = colors => async dispatch => {
try {
setLoading();
const res = await fetch(`/api/settings/${colors.id} `, {
method: "PUT",
body: JSON.stringify(colors),
headers: {
"Content-Type": "application/json"
}
});
const data = await res.json();
dispatch({
type: UPDATE_COLORS,
payload: data
});
} catch ...
Redux reducer:
case UPDATE_COLORS:
return {
...state,
settings: state.settings.map(setting =>
setting._id === action.payload._id ? action.payload : setting
),
loading: false
};
it gives me back:
UnhandledPromiseRejectionWarning: TypeError: Cannot destructure property `NormalPColor` of 'undefined' or 'null'.
[0] at router.put (C:\Users\Marco\Desktop\React-Course\to-do-list\routes\settings.js:81:7)
This happens despite I commented the line 81
Any Idea of my mistakes?
thanks!
It sounds odd but now works I don't know what I have done but now updates
Related
I want to submit a form into mongoDB using nodejs API & reactJs. With the exception of the multiple select option, everything is operating as it should be.
Being new to react, I have no idea how to handle the multi select option's onChange method.
Here is what I've tried:
import React, { useState, useRef } from "react";
import { useForm } from "react-hook-form";
import { v4 as uuidv4 } from 'uuid';
import axios from "axios";
import Select from 'react-select';
export default function EventForm(props) {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm();
const form = useRef();
const [loading, setLoading] = useState(false);
const [info, setInfo] = useState("");
const [analysis, setAnalysis] = useState("Undefined");
const [relatedEvent, setRelatedEvent] = useState([]);
const handleInfoChange = (e) => {
setInfo(e.target.value)
}
const handleAnalysisChange = (e) => {
setAnalysis(e.target.value)
}
const handleRelatedEvents = (e) => {
setRelatedEvent(e.target.value)
}
const relatedEventsData = props.data.map(opt => ({ label: opt.info, value: opt._id }));
const onSubmit = async () => {
setLoading(true);
const MySwal = withReactContent(Swal);
const eventData = {
UUID: uuidv4(),
info: info,
analysis: analysis,
relatedEvent: relatedEvent,
}
axios
.post(`${process.env.REACT_APP_PROXY}/api/events`, eventData)
.then((res) => {
console.log(res);
setLoading(false);
MySwal.fire(
"Success!",
"A new event has been saved successfully",
"success"
);
})
.catch((error) => {
console.log(error);
});
};
return (
<div className="panel-body">
<Form
ref={form}
onSubmit={handleSubmit(onSubmit)}
className="form-horizontal"
>
<div className="row">
<div className="col-lg-6">
<div className="mb-3">
<Form.Label>Info</Form.Label>
<Form.Control
type="text"
placeholder="Enter info..."
{...register("info", { required: true })}
value={info}
onChange={handleInfoChange}
/>
{errors.info && (
<ul className="parsley-errors-list filled" id="parsley-id-7" aria-hidden="false">
<li className="parsley-required">This value is required.</li>
</ul>
)}
</div>
</div>
<div className="col-lg-6">
<div className="mb-3">
<Form.Label>Related events</Form.Label>
<Select
options={relatedEventsData}
value={relatedEvent}
isMulti
onChange={handleRelatedEvents}
/>
</div>
</div>
<div className="col-lg-12">
<Button variant="primary" type="submit">
{loading ? "Saving..." : "Save"}
</Button>
</div>
</div>
</Form>
</div>
);
}
Could you please guide me how to make it work!
Thank you
you can make use of Select onChange event handler which passes the selected options as an array as argument ..
from that you can map over it to get the values as required
something as below:
const handleChange = (opts) => {
const selectedValues = opts.map((opt) => opt.value);
setSelectedValues(selectedValues);
};
Please check the working sample for better clarity 😉 -
I am new to react development. I am using redux to update my product. I had succeeded in updating the product from postman but I couldn't update from the admin panel. I think there is an issue in the front end as API is tested using postman and the backend is working. Kindly help me.
My Product component
import { Link } from "react-router-dom";
import "./Product.css";
import { useLocation } from "react-router-dom";
import axios from "axios";
import { Publish } from '#mui/icons-material';
import { useSelector } from "react-redux";
import { updateProduct } from "../../Redux/apiCalls";
import { useDispatch } from "react-redux";
import { useState } from "react";
const Product = () => {
const location=useLocation();
const productId=location.pathname.split("/")[2];
const product=useSelector(state=>state.product.products.find(product=>product._id===productId))
const dispatch=useDispatch();
console.log(productId);
// const [title, setTitle]=useState("");
// const [desc, setDesc]=useState("");
// const [price, setPrice]=useState("");
// const [inStock, setInStock]=useState("");
const [inputs, setInputs]=useState({})
const handleChange=(e)=>{
setInputs((prev) => {
return { ...prev, [e.target.name]: e.target.value };
});
}
console.log(inputs);
const handleUpdate=async()=>{
await updateProduct(productId, inputs , dispatch)
// await axios.put('http://localhost:5000/api/products/`$productId`', inputs, productId)
// .then(response=>console.log(response.data))
}
return ( <>
<div className="product">
<div className="productTitleContainer">
<h1 className="productTitle">Product</h1>
<Link to="/newproduct">
<button className="productAddButton">Create</button>
</Link>
</div>
<div className="productTop">
<div className="productTopRight">
<div className="productInfoTop">
{/* <img src={product.img} alt="" className="productInfoImg" /> */}
<span className="productName">Product Name</span>
</div>
<div className="productInfoBottom">
<div className="productInfoItem">
<span className="productInfoKey">id:</span>
<span className="productInfoValue">{product._id}</span>
</div>
<div className="productInfoItem">
<span className="productInfoKey">sales:</span>
<span className="productInfoValue">5123</span>
</div>
<div className="productInfoItem">
<span className="productInfoKey">Price</span>
<span className="productInfoValue">{product.price}</span>
</div>
<div className="productInfoItem">
<span className="productInfoKey">in stock:</span>
<span className="productInfoValue">{product.inStock}</span>
</div>
</div>
</div>
</div>
<div className="productBottom">
<form className="productForm">
<div className="productFormLeft">
<label>Product Name</label>
<input type="text" placeholder={product.title} name="title" onChange={handleChange} />
<label>Product Description</label>
<input type="text" placeholder={product.desc} name="desc" onChange={handleChange} />
<label>Product Price</label>
<input type="text" placeholder={product.price} name="price" onChange={handleChange}/>
<label>In Stock</label>
<select name="inStock" id="idStock"
onChange={handleChange}
>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<div className="productFormRight">
<div className="productUpload">
<img src={product.img} alt="" className="productUploadImg"
/>
<label for="file">
<Publish/>
</label>
<input type="file" id="file" style={{display:"none"}} />
</div>
<button className="productButton" onClick={(e)=>handleUpdate(e.preventDefault())}>Update</button>
</div>
</form>
</div>
</div>
</> );
}
export default Product;
My Update component in Redux
export const updateProduct = async (id, product, dispatch) => {
dispatch(updateProductStart());
try {
const res = await userRequest.put(`/products/${id}`);
dispatch(updateProductSuccess({ id, product }));
} catch (err) {
dispatch(updateProductFailure());
}
};
updateProductStart: (state) => {
state.isFetching = true;
state.error = false;
},
updateProductSuccess: (state, action) => {
state.isFetching = false;
state.products[
state.products.findIndex((item) => item._id === action.payload.id)
] = action.payload.product;
},
updateProductFailure: (state) => {
state.isFetching = false;
state.error = true;
},
My API
import { ConstructionOutlined } from "#mui/icons-material";
import axios from "axios";
const BASE_URL = "http://localhost:5000/api/";
const user = JSON.parse(localStorage.getItem("persist:root"))?.user;
const currentUser = user && JSON.parse(user).currentUser;
const TOKEN = currentUser?.accessToken;
// const TOKEN = JSON.parse(JSON.parse(localStorage.getItem("persist:root")).user).currentUser.accessToken;
export const publicRequest = axios.create({
baseURL: BASE_URL,
});
export const userRequest = axios.create({
baseURL: BASE_URL,
headers: { token: `Bearer ${TOKEN}` },
});
My routes
router.put("/:id", verifyTokenAndAdmin, async (req, res) => {
try {
const updatedProduct = await Product.findByIdAndUpdate(
req.params.id,
{
$set: req.body,
},
{ new: true }
);
res.status(200).json(updatedProduct)
} catch (err) {
console.log("Can't update")
}
});
Model
const mongoose = require("mongoose");
const ProductSchema = new mongoose.Schema(
{
title: { type: String, required: true, unique: true },
desc: { type: String, required: true },
img: { type: String},
categories: { type: Array },
size: { type: Array },
color: { type: Array },
price: { type: Number, required: true },
inStock: { type: Boolean, default: true },
},
{ timestamps: true }
);
module.exports = mongoose.model("Product", ProductSchema);
Browsers won't allow cross origin requests if backend doesn't send response with header
Access-Control-Allow-Origin: <current domain name>
.
So at backend we need to send response with header Access-Control-Allow-Origin: localhost:3000
-> In express project:
Run
npm install cors
Change express initialization like this
var express = require('express')
var cors = require('cors')
var app = express()
app.use(cors())
This would add a header Access-Control-Allow-Origin: * to each response handled. This header says allow requests from browser within any website
Check more at
https://expressjs.com/en/resources/middleware/cors.html
https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
Not sure where I am going wrong here. editMedicine should be a function but it is not. When I click the edit medicine button, I get an error onSubmit. I know in other areas of my code I had some misspellings and capitalization issues, I am hoping this is the case here . I have been looking at this for so long that I cant quite spot whats going on.
import React, { Fragment, useState, useContext, useEffect } from "react";
import { GlobalContext } from "../context/GlobalState.js";
import { useHistory, Link } from "react-router-dom";
const Editmedicine = (route) => {
let history = useHistory();
const { medicines, editMedicine } = useContext(GlobalContext);
const [selectedUser, setSeletedUser] = useState({
id: null,
name: "",
directions: "",
});
const currentUserId = route.match.params.id;
useEffect(() => {
const medicineId = currentUserId;
const selectedUser = medicines.find(
(med) => med.id === parseInt(medicineId)
);
setSeletedUser(selectedUser);
}, []);
const onSubmit = (e) => {
e.preventDefault();
editMedicine(selectedUser);
history.push("/");
};
const handleOnChange = (userKey, value) =>
setSeletedUser({ ...selectedUser, [userKey]: value });
if (!selectedUser || !selectedUser.id) {
return <div>sdf</div>;
}
return (
<Fragment>
<div id="editContainer">
<form onSubmit={onSubmit}>
<div className="w-full mb-5">
<label
htmlFor="name"
>
Name of medicine
</label>
<input
value={selectedUser.name}
onChange={(e) => handleOnChange("name", e.target.value)}
type="text"
placeholder="Enter name"
/>
</div>
<div className="w-full mb-5">
<label
htmlFor="directions"
>
Directions
</label>
<input
value={selectedUser.directions}
onChange={(e) => handleOnChange("directions", e.target.value)}
type="text"
placeholder="Enter directions"
/>
</div>
<div className="flex items-center justify-between">
<button id="editMedicine">
Edit medicine
</button>
</div>
<div className="text-center mt-4 text-gray-500">
<Link to="/">Cancel</Link>
</div>
</form>
</div>
</Fragment>
);
};
export default Editmedicine;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
// here is the globalState file
import React, { createContext, useReducer } from "react";
import AppReducer from "./AppReducer";
//start by setting the initial state
const initialState = {
medicines: [
{
id: 1,
name: "Daily Multivitamin",
directions: "Take once a day with 2 glasses of water"
},
{
id: 2,
name: "Herbal capsule",
directions: "Take once a day with food"
},
{
id: 3,
name: "Tincture",
directions: "1 drop a day, 3x a day."
}
]
};
//create context
export const GlobalContext = createContext(initialState);
//provider component
export const GlobalProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
function removeMedicine(id) {
dispatch({
type: "REMOVE_MEDICINE",
payload: id
});
}
//this might have to be ADD_MEDICINE or S instead
function addMedicine(medicines) {
dispatch({
type: "ADD_MEDICINE",
payload: medicines
});
}
function editMedicine(medicines) {
dispatch({
type: "EDIT_MEDICINE",
payload: medicines
});
}
return (
<GlobalContext.Provider
value={{
medicines: state.medicines,
removeMedicine,
addMedicine,
editMedicine
}}
>
{children}
</GlobalContext.Provider>
);
};
Looking for a gentle push in the right direction. Working on a react project and using hooks. Yes, have read documents, but not fully understanding yes.
The ask is about a login routine. Login form works, but does not reflect failed login state until repeat submission; so I am getting previous state, not current.
Tried useEffect...no change. Code follows, and appreciated any constructive feedback:
From the Login form
import React, { useState, useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Redirect } from 'react-router-dom'
import getAuthStatus from 'common/cyclone/auth/authenticated.status'
import {
authenticateByLogin,
authenticationSelector,
} from '../services/auth.service'
import Form from 'react-validation/build/form'
import Input from 'react-validation/build/input'
import CheckButton from 'react-validation/build/button'
const required = (value) => {
if (!value) {
return (
<div className="alert alert-danger" role="alert">
This field is required!
</div>
)
}
}
const Login = (props) => {
const form = useRef()
const checkBtn = useRef()
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState(null)
const dispatch = useDispatch()
const { session, hasErrors } = useSelector(authenticationSelector)
useEffect(() => {}, [session, hasErrors])
const onChangeUsername = (e) => {
const username = e.target.value
setUsername(username)
}
const onChangePassword = (e) => {
const password = e.target.value
setPassword(password)
}
const handleLogin = (e) => {
e.preventDefault()
setLoading(true)
form.current.validateAll()
if (checkBtn.current.context._errors.length === 0) {
dispatch(authenticateByLogin(username, password))
.then(() => {
setLoading(false)
if (hasErrors) {
setErrorMessage(session.error.message)
} else {
//props.history.push('/profile')
// window.location.reload()
}
})
.catch(() => {
setLoading(false)
})
} else {
setLoading(false)
}
}
if (session.success) {
//console.log(session.success)
return <Redirect to="/profile" />
}
if (getAuthStatus()) {
return <Redirect to="/profile" />
}
return (
<div className="col-md-12">
<div className="card card-container">
<img
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
alt="profile-img"
className="profile-img-card"
/>
<Form onSubmit={handleLogin} ref={form}>
<div className="form-group">
<label htmlFor="username">Username</label>
<Input
type="text"
className="form-control"
name="username"
value={username}
onChange={onChangeUsername}
validations={[required]}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Input
type="password"
className="form-control"
name="password"
value={password}
onChange={onChangePassword}
validations={[required]}
/>
</div>
<div className="form-group">
<button className="btn btn-primary btn-block" disabled={loading}>
{loading && (
<span className="spinner-border spinner-border-sm"></span>
)}
<span>Login</span>
</button>
</div>
{hasErrors && (
<div className="form-group">
<div className="alert alert-danger" role="alert">
{errorMessage}
</div>
</div>
)}
<CheckButton style={{ display: 'none' }} ref={checkBtn} />
</Form>
</div>
</div>
)
}
export default Login
From the auth slice:
/** Third Party Libraries */
import { createSlice } from '#reduxjs/toolkit'
import qs from 'qs'
/**Axios Wrapper...nothing fancy here*/
import CycloneAPIInstance from 'common/cyclone/api/api.client'
import CycloneConfig from 'config/base'
/** Main API Server URL */
const API_URL = CycloneConfig.API_URL
const session = JSON.parse(localStorage.getItem('authentication'))
/** Define Initial State */
export const initialState = session
? {
hasErrors: false,
session: session,
}
: {
hasErrors: false,
session: [],
}
/** Define Slice */
const authenticationSlice = createSlice({
name: 'authentication',
initialState,
reducers: {
authenticateUser: (state) => {
state.hasErrors = false
},
authenticateUserSuccess: (state, { payload }) => {
state.hasErrors = false
state.session = payload
console.log(state.session)
},
authenticateUserFailure: (state, { payload }) => {
state.hasErrors = true
state.session = payload
},
deauthenticateUser: (state) => {
state.session = []
},
},
})
export const {
authenticateUser,
authenticateUserSuccess,
authenticateUserFailure,
deauthenticateUser,
} = authenticationSlice.actions
export const authenticationSelector = (state) => state.authentication
export default authenticationSlice.reducer
export function authenticateByLogin(user_name, user_password) {
let requestBody = {
user_name: user_name,
user_password: user_password,
}
let config = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
return async (dispatch) => {
dispatch(authenticateUser())
try {
const response = await CycloneAPIInstance.post(
API_URL + 'auth/login',
qs.stringify(requestBody),
config
)
//console.log(response.data.content)
localStorage.setItem('session', JSON.stringify(response.data.content))
dispatch(authenticateUserSuccess(response.data.content))
} catch (error) {
//console.log(JSON.stringify(error.response.data))
dispatch(authenticateUserFailure(error.response.data))
}
}
}
export function deauthenticateByLogout() {
return async (dispatch) => {
dispatch(deauthenticateUser())
localStorage.removeItem('session')
}
}
Try to set the message when hasError change
useEffect(()=> {
if(hasErrors) {
setErrorMessage(session.error.message)
}
}, [hasErrors]);
This is quite some code so I just skipped through to fix the problem and not pick everything apart. My best guess is this part:
dispatch(authenticateByLogin(username, password))
.then(() => {
setLoading(false)
if (hasErrors) {
setErrorMessage(session.error.message)
} else {
//props.history.push('/profile')
// window.location.reload()
}
})
.catch(() => {
setLoading(false)
})
Here you execute the async authentication and then do thing based on "hasError". This "hasError" comes from a hook. We (or at least I) have no clear idea how this is managed. Thing is you cant be 100% sure that hasError is really trustworthy at the point you check it in the then-block. The hook might run just after the next render, which explains why you see the previous state, not the actual one.
Best guess would be to use the response from that async call, because there should be one => authenticate.then((response) => if(response.hasError) ...)
With this check you can set your own error state and your component should be up-to-date
Let me know if this fixes your error.
I have the following form:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { updateExpert, fetchExpert } from "../../store/actions/expertActions";
class ExpertForm extends Component {
state = {
expert: {}
};
componentWillMount() {
console.log("ComponentWillMount");
const id = this.props.match.params.id;
console.log("Will fetch expert with id", id);
this.props.fetchExpert(id);
}
handleChange = e => {
console.log(e);
this.setState({
expert: {
...this.state.expert,
[e.target.id]: e.target.value
}
});
};
componentWillReceiveProps(nextProps) {
const newExpert = nextProps.expert;
console.log("got new expert ", newExpert);
this.setState({
expert: nextProps.expert
});
}
handleSubmit = e => {
e.preventDefault();
const originalExpert = this.props.expert;
console.log("Expert before", originalExpert);
// const updatedExpert = {
// firstName: this.state.expert.firstName,
// lastName: this.state.expert.lastName,
// bio: this.state.expert.bio,
// country: originalExpert.country,
// interestIds: originalExpert.interestIds,
// city: originalExpert.city,
// summary: originalExpert.summary,
// websiteText: originalExpert.websiteText,
// websiteUrl: originalExpert.websiteUrl
// };
const updatedExpert = this.state.expert;
console.log("Expert after", updatedExpert);
//call action
this.props.updateExpert(originalExpert.userId, updatedExpert);
};
render() {
const { expert } = this.props;
return (
<div className="container">
<div className="card">
<form onSubmit={this.handleSubmit} className="white">
<div className="card-content">
<h5 className="grey-text text-darken-3">Update expert</h5>
<div className="row">
<div className="input-field col s6">
<label htmlFor="firstName">First Name</label>
<input
onChange={this.handleChange}
type="text"
id="firstName"
/>
</div>
<div className="input-field col s6">
<label htmlFor="lastName">Last Name</label>
<input
onChange={this.handleChange}
type="text"
id="lastName"
/>
</div>
</div>
<div className="input-field">
<label htmlFor="bio">Bio</label>
<textarea
className="materialize-textarea"
id="bio"
onChange={this.handleChange}
/>
</div>
<div className="input-field">
<label htmlFor="summary">Summary</label>
<textarea
className="materialize-textarea"
id="summary"
onChange={this.handleChange}
/>
</div>
<div className="row">
<div className="input-field col s6">
<label htmlFor="country">Country</label>
<textarea
className="materialize-textarea"
id="country"
onChange={this.handleChange}
/>
</div>
<div className="input-field col s6">
<label htmlFor="city">City</label>
<textarea
className="materialize-textarea"
id="city"
onChange={this.handleChange}
/>
</div>
</div>
<div className="row">
<div className="input-field col s6">
<label htmlFor="websiteText">Website text</label>
<textarea
className="materialize-textarea"
id="websiteText"
onChange={this.handleChange}
/>
</div>
<div className="input-field col s6">
<label htmlFor="websiteUrl">Website URL</label>
<textarea
className="materialize-textarea"
id="websiteUrl"
onChange={this.handleChange}
/>
</div>
</div>
</div>
<div className="card-action">
<div className="input-field">
<button className="btn pink lighten-1 z-depth-0">Update</button>
</div>
</div>
</form>
</div>
</div>
);
}
}
const mapStateToProps = state => ({
expert: state.experts.item
});
const mapDispatchToProps = dispatch => {
return {
updateExpert: (id, expert) => dispatch(updateExpert(id, expert)),
fetchExpert: id => dispatch(fetchExpert(id))
};
};
export default connect(
mapStateToProps, //mapstatetoprops
mapDispatchToProps //mapdispatchtoprops
)(ExpertForm);
Now this form is used mostly to edit an item of the Expert type, not adding it. Which means I should prefill it with the information already stored in the database.
However when I try to set the value directly on an input like so:
<input
value={expert.firstName}
onChange={this.handleChange}
type="text"
id="firstName"
/>
I get the following error:
index.js:1452 Warning: A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
This is the ExpertList component from which the user accesses this ExpertForm:
import React, { Component } from "react";
import PropTypes from "prop-types";
import ExpertItem from "./expert-item";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { fetchExperts } from "../../store/actions/expertActions";
class ExpertList extends Component {
componentWillMount() {
console.log("ComponentWillMount");
this.props.fetchExperts();
}
componentWillReceiveProps(nextProps) {
console.log("Rceived new props");
}
render() {
const { experts } = this.props;
const expertsDom = experts.map(expert => (
<Link to={"/expert/edit/" + expert.userId}>
<ExpertItem key={expert.userId} expert={expert} />
</Link>
));
return <div className="expert-list section">{expertsDom}</div>;
}
}
const mapStateToProps = state => ({
experts: state.experts.items
});
export default connect(
mapStateToProps,
{ fetchExperts }
)(ExpertList);
These are my actions :
import {
FETCH_EXPERTS,
UPDATE_EXPERT,
ADD_EXPERT,
FETCH_EXPERT
} from "./types";
import axios from "../../network/axios";
export const createExpert = expert => {
return (dispatch, getState) => {
//make async call to database
dispatch({ type: ADD_EXPERT, expert: expert });
// type: ADD_EXPERT;
};
};
export const fetchExpert = id => {
return (dispatch, getState) => {
console.log("fetching expert with id ", id);
axios
.get("/connections/experts")
.then(response => {
const selectedExpert = response.data.filter(e => {
return e.userId === id;
})[0];
console.log("ExpertsData ", selectedExpert);
// const newState = Object.assign({}, this.state, {
// experts: newExperts
// });
dispatch({
type: FETCH_EXPERT,
payload: selectedExpert
});
})
.catch(error => {
console.log(error);
});
};
};
//Thunk allows us to call dispatch directly so that we can make async requests
//We can consider dispatch a resolver/promise, calling dispatch is just sending
//the data back
export const fetchExperts = () => {
return (dispatch, getState) => {
console.log("fetching");
console.log("getstate ", getState());
const accessToken = getState().auth.authInfo.accessToken;
console.log("authToken ", accessToken);
axios
.get("/connections/experts")
.then(response => {
const newExperts = response.data;
console.log("ExpertsData ", newExperts);
// const newState = Object.assign({}, this.state, {
// experts: newExperts
// });
dispatch({
type: FETCH_EXPERTS,
payload: newExperts
});
})
.catch(error => {
console.log(error);
});
};
};
export const updateExpert = (id, expertData) => {
return dispatch => {
console.log("updating expert", id, expertData);
axios
.put("/experts/" + id, expertData)
.then(response => {
const updatedExpert = response.data;
dispatch({
type: UPDATE_EXPERT,
payload: updatedExpert
});
})
.catch(error => {
console.log(error);
});
};
};
And this is my reducer:
import {
FETCH_EXPERTS,
UPDATE_EXPERT,
FETCH_EXPERT
} from "../../store/actions/types";
const initialState = {
items: [],
item: {}
};
const expertReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_EXPERT:
console.log("reducer fetch by id");
return {
...state,
item: action.payload
};
case FETCH_EXPERTS:
console.log("reducer fetch");
return {
...state,
items: action.payload
};
case UPDATE_EXPERT:
console.log("reducer update");
return {
...state,
item: action.payload
};
default:
return state;
}
};
export default expertReducer;
Instead of using value property, You need to use defaultValue as described here in Default Values section if You want to have a default value for input field.
The problem is that your value is undefined before Redux's state is loaded. You can solve this by giving it an empty string by default, something like this:
<input
value={typeof expert.firstName === 'undefined' ? '' : expert.firstName}
onChange={this.handleChange}
type="text"
id="firstName"
/>