Hey guy's I have no idea why my state isn't updating in the DOM, I'm sure I'm missing some key react principal. Heres the photo of what my DOM looks like after I submit a post
Instead of the post displaying when I click submit with filled out info, a blank post is shown. And I have to manually reload the page before it shows what was added. I think I'm actually battling some sync issues, please let me know what I can do if you see anything.
I will put the most relevant file's code below and also attache the repository at the bottom if you want to have a complete look.
dataActions.js
import { SET_POSTS, LOADING_DATA, DELETE_POST, POST_PRODUCT, SET_ERRORS, CLEAR_ERRORS, LOADING_UI } from "../types";
import axios from 'axios';
//GET ALL PRODUCTS
export const getPosts = () => dispatch => {
dispatch({ type: LOADING_DATA });
axios.get('/posts')
.then(res => {
dispatch({
type: SET_POSTS,
payload: res.data
})
})
.catch(err => {
dispatch({
type: SET_POSTS,
payload: []
})
})
}
//POST PRODUCT
export const postProduct = (newPost) => (dispatch) => {
dispatch({ type: LOADING_UI });
axios.post('/post', newPost)
.then(res => {
dispatch({
type: POST_PRODUCT,
payload: res.data
})
console.log("success");
dispatch({ type: CLEAR_ERRORS })
})
.catch(err => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
})
})
}
//DELETE PRODUCT
export const deletePost = (postId) => (dispatch) => {
axios.delete(`/post/${postId}`)
.then(() => {
dispatch({ type: DELETE_POST, payload: postId })
})
.catch(err => console.log(err))
}
dataReducer.js
import { SET_POSTS } from '../types';
import { LOADING_DATA, DELETE_POST, POST_PRODUCT/*, SET_POST*/ } from '../types';
const initialState = {
posts: [],
post: {},
loading: false
};
export default function(state = initialState, action){
switch(action.type){
case LOADING_DATA:
return {
...state,
loading: true
}
case SET_POSTS:
return{
...state,
posts: action.payload,
loading: false
}
case DELETE_POST:
let index = state.posts.findIndex(post => post.postId === action.payload);
state.posts.splice(index, 1);
return {
...state
}
case POST_PRODUCT:
return {
...state,
posts: [action.payload, ...state.posts]
}
default:
return state
}
}
PostProduct.js
import React, { Component, Fragment } from "react";
import { withStyles } from "#material-ui/core/styles";
import PropTypes from "prop-types";
import MyButton from "../util/MyButton";
//MUI Stuff
import Button from "#material-ui/core/Button";
import TextField from "#material-ui/core/TextField";
import Dialog from "#material-ui/core/Dialog";
import DialogTitle from "#material-ui/core/DialogTitle";
import DialogContent from "#material-ui/core/DialogContent";
import DeleteOutline from "#material-ui/icons/DeleteOutline";
import CircularProgress from '#material-ui/core/CircularProgress';
import AddIcon from '#material-ui/icons/Add';
import CloseIcon from "#material-ui/icons/Close";
//REDUX
import { connect } from "react-redux";
import { postProduct } from "../redux/actions/dataActions";
const styles = {
form: {
textAlign: "center"
},
image: {
margin: "20px auto 20px auto",
width: "50px"
},
pageTitle: {
margin: "10px auto 10px auto"
},
textField: {
margin: "10px auto 10px auto"
},
button: {
marginTop: 20,
postition: "relative"
},
customError: {
color: "red",
fontSixe: "0.8rem",
marginTop: 10
},
progress: {
position: "absolute"
},
submitButton: {
position: "relative"
},
progressSpinner: {
position: 'absolute'
},
closeButton: {
position: 'absolute',
left: '90%',
top: '10%'
}
};
class PostProduct extends Component {
state = {
open: false,
name: '',
errors: {}
};
UNSAFE_componentWillReceiveProps(nextProps){
if (nextProps.UI.errors) {
this.setState({
errors: nextProps.UI.errors
})
}
}
handleOpen = () => {
this.setState({ open: true })
}
handleClose = () => {
this.setState({ open: false })
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value })
}
handleSubmit = (event) => {
event.preventDefault();
this.props.postProduct({ body: this.state.body })
}
render(){
const { errors } = this.state;
const { classes, UI: {loading }} = this.props;
return (
<Fragment>
<MyButton onClick={this.handleOpen} tip="Post a Product">
<AddIcon />
</MyButton>
<Dialog
open={this.state.open}
onClose={this.handleClose}
fullWidth
maxWidth="sm"
>
<MyButton
tip="close"
onClick={this.handleClose}
tipClassName={classes.closeButton}
>
<CloseIcon />
</MyButton>
<DialogTitle>Post the new Product</DialogTitle>
<DialogContent>
<form onSubmit={this.handleSubmit}>
<TextField
name="name"
type="text"
lable="Post Product"
multiline
rows="3"
placeholder="name"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="images"
type="text"
lable="image"
multiline
rows="3"
placeholder="image"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="itemCategory"
type="text"
lable="Painting"
multiline
rows="3"
placeholder="Painting"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="link"
type="text"
lable="link"
multiline
rows="3"
placeholder="https://etsy.com"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="info"
type="text"
lable="blah blah blah"
multiline
rows="3"
placeholder="info"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="price"
type="text"
lable="Price"
multiline
rows="3"
placeholder="75.99"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="available"
type="text"
lable="available?"
multiline
rows="3"
placeholder="true"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<TextField
name="highEnd"
type="text"
lable="High-end or not?"
multiline
rows="3"
placeholder="false"
error={errors.body ? true : false}
helperText={errors.body}
className={classes.textFields}
onChange={this.handleChange}
fullWidth
/>
<Button
type="submit"
variant="contained"
color="primary"
className={classes.submitButton}
disabled={loading}
>
Submit
{loading && (
<CircularProgress
size={30}
className={classes.progressSpinner}
/>
)}
</Button>
</form>
</DialogContent>
</Dialog>
</Fragment>
);
}
} // END CLASS
PostProduct.propTypes = {
postProduct: PropTypes.func.isRequired,
UI: PropTypes.object.isRequired
}
const mapStateToProps = (state) => ({
UI: state.UI
})
export default connect(mapStateToProps, { postProduct })(withStyles(styles)(PostProduct))
The front end code is in this repository here: https://github.com/jIrwinCline/planum-front
Thanks for any help. I know this is a big question...
Post product thunk sets LOADING_UI and then POST_PRODUCT if successful.
export const postProduct = (newPost) => (dispatch) => {
dispatch({ type: LOADING_UI });
axios.post('/post', newPost)
.then(res => {
dispatch({
type: POST_PRODUCT,
payload: res.data
})
console.log("success");
dispatch({ type: CLEAR_ERRORS })
})
.catch(err => {
dispatch({
type: SET_ERRORS,
payload: err.response.data
})
})
In your reducer, there is no LOADING_UI case and thePOST_PRODUCT case just sets the post data but doesn't turn loading off:
case POST_PRODUCT:
return {
...state,
posts: [action.payload, ...state.posts]
}
I suspect you have to add a LOADING_UI case to your reducer and ensure that POST_PRODUCT sets loading to false when it updates your store with the new posts.
Related
Im using Antd library and i can't seem to find where i have the bug.
This is my EditableTableCell component
import React, {Component} from 'react';
import { Form } from '#ant-design/compatible';
import '#ant-design/compatible/assets/index.css';
import { Input, InputNumber, Select, DatePicker } from "antd";
import moment from "moment";
import {EditableContext} from "./EditableTableRow";
const FormItem = Form.Item;
const Option = Select.Option;
class EditableTableCell extends Component {
getInput = (record, dataIndex, title, getFieldDecorator) => {
switch (this.props.inputType) {
case "number":
return (
<FormItem style={{ margin: 0 }}>
{getFieldDecorator(dataIndex, {
rules: [
{
required: true,
message: `Please Input ${title}!`
}
],
initialValue: record[dataIndex]
})(
<InputNumber formatter={value => value} parser={value => value} />
)}
</FormItem>
);
case "date":
return (
<FormItem style={{ margin: 0 }}>
{getFieldDecorator(dataIndex, {
initialValue: moment(record[dataIndex], this.dateFormat)
})(<DatePicker format={this.dateFormat} />)}
</FormItem>
);
case "select":
return (
<FormItem style={{ margin: 0 }}>
{getFieldDecorator(dataIndex, {
initialValue: record[dataIndex]
})(
<Select style={{ width: 150 }}>
{[...Array(11).keys()]
.filter(x => x > 0)
.map(c => `Product ${c}`)
.map((p, index) => (
<Option value={p} key={index}>
{p}
</Option>
))}
</Select>
)}
</FormItem>
);
default:
return (
<FormItem style={{ margin: 0 }}>
{getFieldDecorator(dataIndex, {
rules: [
{
required: true,
message: `Please Input ${title}!`
}
],
initialValue: record[dataIndex]
})(<Input />)}
</FormItem>
);
}
}
render() {
const { editing, dataIndex, title, inputType, record, index,...restProps} = this.props;
return (
<EditableContext.Consumer>
{form => {
const { getFieldDecorator } = form;
return (
<td {...restProps}>
{editing ?
this.getInput(record, dataIndex, title, getFieldDecorator)
: restProps.children}
</td>
);
}}
</EditableContext.Consumer>
);
}
}
export default EditableTableCell;
This is my EditableTableCell component
import React, {Component} from 'react';
import { Form} from '#ant-design/compatible';
export const EditableContext = React.createContext();
class EditableTableRow extends Component {
render() {
return (
<EditableContext.Provider value={this.props.form}>
<tr {...this.props} />
</EditableContext.Provider>
);
}
}
export default EditableTableRow=Form.create()(EditableTableRow);
This is my ProductsPage component im having bug in
import React, {Component} from 'react';
import {Button, Layout, notification, Popconfirm, Space, Table,Typography} from "antd";
import {Link} from "react-router-dom";
import {Content} from "antd/es/layout/layout";
import EditableTableRow, {EditableContext} from "../components/EditableTableRow";
import EditableTableCell from "../components/EditableTableCell";
import API from "../server-apis/api";
import {employeesDataColumns} from "../tableColumnsData/employeesDataColumns";
import {CheckCircleFilled, InfoCircleFilled} from "#ant-design/icons";
class ProductsPage extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
error: null,
isLoaded: false,
editingKey: "",
errorMessage: "",
}
}
columns = [
...employeesDataColumns,
{
title: "Actions",
dataIndex: "actions",
width: "10%",
render: (text, record) => {
const editable = this.isEditing(record);
return editable ? (
<span>
<EditableContext.Consumer>
{form => (<a onClick={() => this.saveData(form, record.username)} style={{ marginRight: 8 }}>Save</a>)}
</EditableContext.Consumer>
<a onClick={this.cancel}>Cancel</a>
</span>
) : (
<Space size="middle">
<a onClick={() => this.edit(record.username)}>Edit</a>
<Popconfirm title="Are you sure you want to delete this product?"
onConfirm={() => this.remove(record.username)}>
<a style={{color:"red"}}>Delete</a>
</Popconfirm>
</Space>
);
},
}
];
isEditing = (record) => {
return record.username === this.state.editingKey;
};
edit(username) {
this.setState({editingKey:username});
}
cancel = () => {
this.setState({ editingKey: ""});
};
componentDidMount() {
this.setState({ loading: true });
const token="Bearer "+ JSON.parse(localStorage.getItem("token"));
API.get(`users/all`,{ headers: { Authorization: token}})
.then(res => {
// console.log(res.data._embedded.productList);
const employees = res.data._embedded.employeeInfoDtoList;
this.setState({loading: false,data:employees });
})
}
async remove(username) {
const token="Bearer "+ JSON.parse(localStorage.getItem("token"));
API.delete(`/users/${username}`,{ headers: { Authorization: token}})
.then(() => {
let updatedProducts = [...this.state.data].filter(i => i.username !== username);
this.setState({data: updatedProducts});
this.successfullyAdded("Employee is deleted. It wont have any access to the website anymore.")
}).catch(()=>this.errorHappend("Failed to delete"));
}
hasWhiteSpace(s) {
return /\s/g.test(s);
}
saveData(form,username) {
form.validateFields((error, row) => {
if (error) {
return;
}
const newData = [...this.state.data];
const index = newData.findIndex(item => username === item.username);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row
});
const token="Bearer "+ JSON.parse(localStorage.getItem("token"));
const response = API.put(`/users/${username}/update`, row,{ headers: { Authorization: token}})
.then((response) => {
this.setState({ data: newData, editingKey: ""});
this.successfullyAdded("Empolyee info is updated")
})
.catch(error => {
this.setState({ errorMessage: error.message });
this.errorHappend("Failed to save changes.")
console.error('There was an error!', error);
});
});
}
successfullyAdded = (message) => {
notification.info({
message: `Notification`,
description:message,
placement:"bottomRight",
icon: <CheckCircleFilled style={{ color: '#0AC035' }} />
});
};
errorHappend = (error) => {
notification.info({
message: `Notification`,
description:
`There was an error! ${error}`,
placement:"bottomRight",
icon: <InfoCircleFilled style={{ color: '#f53333' }} />
});
};
render() {
const components = {
body: {
row: EditableTableRow,
cell: EditableTableCell
}
};
const columns = this.columns.map(col => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: record => {
const checkInput = index => {
switch (index) {
case "price":
return "number";
default:
return "text";
}
};
return {
record,
// inputType: col.dataIndex === "age" ? "number" : "text",
inputType: checkInput(col.dataIndex),
dataIndex: col.dataIndex,
title: col.title,
editing: this.isEditing(record)
};
}
};
});
const { data, loading } = this.state;
return (
<Layout>
<div>
<Link to="/add-product">
<Button style={{float:"right", background: "#0AC035",marginBottom:"1em", marginTop:"1em" }}
type="primary">New emplyee</Button>
</Link>
</div>
<Content>
<Table components={components} bordered dataSource={data} columns={columns} loading={loading} rowKey={data.username} rowClassName="editable-row"/>
</Content>
</Layout>
);
}
}
export default ProductsPage;
This is the bug I'm having:
enter image description here
And i want to have this result like its shown in Antd docs:
enter image description here
Id really appreciate if you take a look and help me figure out where im wrong
Updated Solution:
I find the issue. In render where you map the columns, you just return the column if it's not an editable column. You can check the code below. I added a check if it's dataIndex === 'actions', then return the following code:
Please Follow the link:
https://react-ts-v3fbst.stackblitz.io
Changes:
1.In columns, i remove the render function from the action object:
{
title: 'Actions',
dataIndex: 'actions',
width: '10%',
},
2. In render function where you map the columns, add the following code before this condition if(!col.editable) {,:
if (col.dataIndex === 'actions') {
return {
...col,
render: (text, record) => {
const editable = this.isEditing(record);
return editable ? (
<span>
<EditableContext.Consumer>
{(form) => (
<a onClick={() => this.saveData(form, record.username)} style={{ marginRight: 8 }}>
Save
</a>
)}
</EditableContext.Consumer>
<a onClick={this.cancel}>Cancel</a>
</span>
) : (
<Space size='middle'>
<a onClick={() => this.edit(record.username)}>Edit</a>
<Popconfirm title='Are you sure you want to delete this product?' onConfirm={() => this.remove(record.username)}>
<a style={{ color: 'red' }}>Delete</a>
</Popconfirm>
</Space>
);
}
};
}
When you click on edit, you set the username as key for that particular row for editing, make sure you have username in each record. I tested this using the following data:
const data = [
{ id: 8, name: 'baun', model: '2022', color: 'black', price: 358, quantity: 3, username: 'brvim' },
{ id: 3, name: 'galileo', model: '20221', color: 'white', price: 427, quantity: 7, username: 'john' }
];
Most important, you should select that attribute as key that is unique in all records. As you are using username, i don't know what is your business logic or data looks like, but technically each record can have same username. So you must select something that would always be unique in your complete data.
I am faced with an issue where when I submit a form, whether it was successful or not, the whole of the form field resets. so I am assuming that is because the component is rerendering.
I want when I submit a form, whether the network request was successful or not, I want it to still maintain the field contents
below is how the entire code looks like:
import { Formik, FieldArray } from 'formik';
import * as Yup from 'yup';
import React, { useEffect } from 'react';
import * as Style from './create-lesson-form.styles';
import { ReactComponent as AddNewIcon } from '../../assets/add-new.svg';
import { ReactComponent as RemoveRecent } from '../../assets/remove-recent.svg';
import CustomButton from '../custom-button/custom-button.component';
import SimpleCheckBox from '../field-inputs/simple-checkbox/simple-checkbox.component';
import { createALessonNote } from '../../redux/lesson_note/lesson_note.actions';
import { LessonNotePayload } from '../../redux/lesson_note/lesson_note.types';
import { connect } from 'react-redux';
import { CreateLessonPropType } from './create-lesson-form.types';
import ToastAlert from '../toast/toast.components';
import { createStructuredSelector } from 'reselect';
import {
selectLessonCreationIsSuccess,
selectLessonNoteError,
selectLessonNoteSuccess,
} from '../../redux/lesson_note/lesson_note.selector';
import { selectSubjects } from '../../redux/subject/subject.selector';
import { fetchSubjectAction } from '../../redux/subject/subject.action';
import { fetchLevelAction } from '../../redux/level/level.action';
import { selectLevels } from '../../redux/level/level.selectors';
const CreateLessonForm: React.FC<CreateLessonPropType> = ({
createLessonNote,
fetchSubjects,
fetchLevels,
lesson_note_error,
lesson_note_success,
isLessonCreated,
subjects,
levels,
}) => {
const handleAddStepsField = (values: any, setValues: any) => {
const steps = [...values.steps];
steps.push('');
setValues({ ...values, steps });
};
const handleAddParagraphField = (values: any, setValues: any) => {
const paragraphs = [...values.paragraphs];
paragraphs.push('');
setValues({ ...values, paragraphs });
};
const handleDeleteParagraphFields = (values: any, setValues: any) => {
const paragraphs = [...values.paragraphs];
paragraphs.pop();
setValues({ ...values, paragraphs });
};
const handleDeleteStepsFields = (values: any, setValues: any) => {
const steps = [...values.steps];
steps.pop();
setValues({ ...values, steps });
};
const fetchFormValues = async (values: any) => {
const lessonPayload: LessonNotePayload = {
class_id: values.class,
subject_id: values.subject,
topic: values.topic,
is_published: values.is_published,
lesson_body: values.paragraphs,
lesson_plan: values.steps,
};
await createLessonNote(lessonPayload);
};
useEffect(() => {
fetchSubjects();
fetchLevels();
}, []);
return (
<Style.CreateLessonNoteContainer>
{lesson_note_error.length ? <ToastAlert message={lesson_note_error} type="failure" /> : null}
{isLessonCreated ? <ToastAlert message={lesson_note_success} type="success" /> : null}
<Formik
/**
*Declare field initial values
*/
initialValues={{
topic: '',
subject: '',
class: '',
is_published: false,
date_of_delivery: '',
paragraphs: [],
steps: [],
}}
/**
* validate form on client side
*/
validationSchema={Yup.object({
topic: Yup.string().required(),
subject: Yup.string().required(),
class: Yup.number().required(),
date_of_delivery: Yup.date().required(),
paragraphs: Yup.array().of(Yup.string().required()),
steps: Yup.array().of(Yup.string().required()),
})}
/**
* listen to user submit action
*/
onSubmit={async (values, { setSubmitting, resetForm }) => {
alert(values);
console.log(values);
await fetchFormValues(values);
if (isLessonCreated) {
resetForm();
console.log('reset form');
}
setSubmitting(false);
}}
>
{({ values, setValues }) => (
<>
<Style.FormTag>
<Style.LessonBodyFooter>
<Style.LessonHeadInput>
<Style.LessonParagraphHeader>
<h3>Lesson Header</h3>
</Style.LessonParagraphHeader>
<Style.CustomTextInput label="Topic" name="topic" type="text" />
<Style.CustomSelectDropdown label="Subject" name="subject">
<option value={''} disabled selected>
select subject
</option>
{subjects.map((subject) => {
return (
<option key={subject.id} value={subject.id}>
{subject.title}
</option>
);
})}
</Style.CustomSelectDropdown>
<Style.CustomSelectDropdown label="Level" name="class">
<option value={''} disabled selected>
select level
</option>
{levels.map((level) => {
return (
<option key={level.id} value={level.id}>
{level.title}
</option>
);
})}
</Style.CustomSelectDropdown>
<Style.CustomTextInput
label="Date of delivery"
name="date_of_delivery"
type="date"
/>
</Style.LessonHeadInput>
<Style.LessonParagraphContainer>
<Style.LessonParagraphHeader>
<h3>Lesson Paragraphs</h3>
<div>
<RemoveRecent
onClick={() => handleDeleteParagraphFields(values, setValues)}
/>
<AddNewIcon onClick={() => handleAddParagraphField(values, setValues)} />
</div>
</Style.LessonParagraphHeader>
<Style.LessonParagraphFieldContainer>
<FieldArray name={'paragraphs'}>
{() =>
values.paragraphs.map((_, index) => {
return (
<Style.LessonParagraphFieldDiv key={index}>
<Style.CustomTextArea
label={`Paragraph ${index + 1}`}
name={`paragraphs.${index}`}
/>
</Style.LessonParagraphFieldDiv>
);
})
}
</FieldArray>
</Style.LessonParagraphFieldContainer>
</Style.LessonParagraphContainer>
<Style.LessonStepContainer>
<Style.LessonParagraphHeader>
<h3>Lesson Steps</h3>
<div>
<RemoveRecent onClick={() => handleDeleteStepsFields(values, setValues)} />
<AddNewIcon onClick={() => handleAddStepsField(values, setValues)} />
</div>
</Style.LessonParagraphHeader>
<Style.LessonParagraphFieldContainer>
<FieldArray name={'steps'}>
{() =>
values.steps.map((_, index) => {
return (
<Style.LessonParagraphFieldDiv key={index}>
<Style.CustomTextArea
label={`Step ${index + 1}`}
name={`steps.${index}`}
/>
</Style.LessonParagraphFieldDiv>
);
})
}
</FieldArray>
</Style.LessonParagraphFieldContainer>
</Style.LessonStepContainer>
</Style.LessonBodyFooter>
<Style.LessonFormFooter>
<SimpleCheckBox name={'is_published'}>Publish Note</SimpleCheckBox>
<CustomButton type="submit">Submit</CustomButton>
</Style.LessonFormFooter>
</Style.FormTag>
</>
)}
</Formik>
</Style.CreateLessonNoteContainer>
);
};
const mapStateToProps = createStructuredSelector({
lesson_note_error: selectLessonNoteError,
isLessonCreated: selectLessonCreationIsSuccess,
lesson_note_success: selectLessonNoteSuccess,
subjects: selectSubjects,
levels: selectLevels,
});
const mapDispatchToProps = (dispatch: any) => {
return {
createLessonNote: ({
class_id,
subject_id,
topic,
is_published,
lesson_body,
lesson_plan,
}: LessonNotePayload) =>
dispatch(
createALessonNote({
class_id,
subject_id,
topic,
is_published,
lesson_body,
lesson_plan,
}),
),
fetchSubjects: () => dispatch(fetchSubjectAction()),
fetchLevels: () => dispatch(fetchLevelAction()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(CreateLessonForm);
I am getting the error
Error: Function components cannot have refs. Did you mean to use
React.forwardRef()?
And if you check my code it is correct. I am also passing even and it says
Line 91: Unexpected use of 'event' no-restricted-globals
If I am doing something wrong I do not know. According to my research I found that there is some version issue maybe or not. I am using the latest version of create react app
import React, { Component } from 'react';
import { AuthUserContext } from '../Session';
import { withFirebase } from '../Firebase';
import NewsList from './NewsList';
import { ValidatorForm, TextValidator } from 'react-material-ui-form-validator';
import { Container, Card, CardContent, Button } from '#material-ui/core';
import Form from 'react-bootstrap/Form';
class News extends Component {
constructor(props) {
super(props);
this.state = {
newsTitle: '',
newsDescription: '',
news: [],
limit: 5,
loading: false,
submitted: false,
error: null,
};
}
componentDidMount() {
this.onListenForNews();
}
onListenForNews = () => {
this.setState({ loading: true });
this.props.firebase
.news()
.orderByChild('createdAt')
.limitToLast(this.state.limit)
.on('value', snapshot => {
const newsObject = snapshot.val();
if (newsObject) {
const newsLis = Object.keys(newsObject).map(key => ({
...newsObject[key],
uid: key,
}));
this.setState({
news: newsLis,
loading: false,
});
} else {
this.setState({ news: null, loading: false });
}
});
};
componentWillUnmount() {
this.props.firebase.news().off();
}
handleChange = (event) => {
this.setState({ [event.target.name]: event.target.value });
};
onCreateNews = (event, authUser) => {
this.props.firebase.news().push({
newsTitle: this.state.newsTitle,
newsDescription: this.state.newsDescription,
userId: authUser.uid,
createdAt: this.props.firebase.serverValue.TIMESTAMP,
});
this.setState({
newsTitle: '',
newsDescription: '',
error: null,
submitted: true,
});
event.preventDefault();
};
onEditNews = (news, newsTitle, newsDescription) => {
this.props.firebase.news(news.uid).set({
...news,
newsTitle,
newsDescription,
editedAt: this.props.firebase.serverValue.TIMESTAMP,
});
};
onRemoveNews = uid => {
this.props.firebase.news(uid).remove();
};
onNextPage = () => {
this.setState(
state => ({ limit: state.limit + 5 }),
this.onListenForNews,
);
};
render() {
const { users } = this.props;
const { newsTitle, newsDescription, news, loading, submitted, error } = this.state;
return (
<AuthUserContext.Consumer>
{authUser => (
<div>
{!loading && news && (
<button type="button" onClick={this.onNextPage}>
More
</button>
)}
{loading && <div>Loading ...</div>}
{news && (
<NewsList
news={news.map(news => ({
...news,
user: users
? users[news.userId]
: { userId: news.userId },
}))}
onEditNews={this.onEditNews}
onRemoveNews={this.onRemoveNews}
/>
)}
{!news && <div>There are no messages ...</div>}
<Container maxWidth="lg">
<ValidatorForm
ref="form"
onSubmit={event =>
this.onCreateNews(event, authUser)
}
>
<div>
{error && (
<div className="alert alert-danger" role="alert">
{error.message}
</div>
)}
<Card>
<CardContent>
<Form.Group>
<TextValidator
label="News Title"
onChange={this.handleChange}
name="newsTitle"
type="text"
value={newsTitle}
variant="outlined"
fullWidth={true}
validators={['required']}
errorMessages={['New title field is required', 'News title is not valid']}
/>
</Form.Group>
<Form.Group>
<TextValidator
label="Description"
onChange={this.handleChange}
name="newsDescription"
type="text"
value={newsDescription}
variant="outlined"
fullWidth={true}
validators={['required']}
errorMessages={['Description field is required']}
/>
</Form.Group>
<Form.Group>
<Button
color="primary"
variant="contained"
type="submit"
fullWidth={true}
size="large"
disabled={submitted}
>
{
(submitted && 'Signing In - Redirecting')
|| (!submitted && 'Sign In')
}
</Button>
</Form.Group>
</CardContent>
</Card>
</div>
</ValidatorForm>
</Container>
</div>
)}
</AuthUserContext.Consumer>
);
}
}
export default withFirebase(News);
I am getting a TypeError for the property of "token" after submitting a form for an account registration (JWT authentication).
I am using React-Redux as well.
Also, is there any other way rather than storing in localStorage ?
Below are the codes for the authentication reducer and action.
authReducer.js
import {
USER_LOADING,
USER_LOADED,
AUTH_ERROR,
REGISTER_SUCCESS,
REGISTER_FAIL
} from '../actions/types';
const initialState = {
token: localStorage.getItem('token'),
isAuthenticated: null,
isLoading: false,
user: null
};
export default function (state = initialState, action) {
switch (action.type) {
case USER_LOADING:
return {
...state,
isLoading: true
};
case USER_LOADED:
return {
...state,
isAuthenticated: true,
isLoading: false,
user: action.payload
};
case LOGIN_SUCCESS:
case REGISTER_SUCCESS:
localStorage.setItem('token', action.payload.token);
return {
...state,
...action.payload,
isAuthenticated: true,
isLoading: false
};
case AUTH_ERROR:
case LOGIN_FAIL:
case LOGOUT_SUCCESS:
case REGISTER_FAIL:
localStorage.removeItem('token');
return {
...state,
token: null,
user: null,
isAuthenticated: false,
isLoading: false
};
default:
return state;
}
}
auth.Actions
import axios from 'axios';
import { returnErrors } from './errorActions';
import {
USER_LOADING,
USER_LOADED,
AUTH_ERROR,
REGISTER_SUCCESS,
REGISTER_FAIL
} from './types';
//check token and load user
export const loadUser = () => (dispatch, getState) => {
//User loading
dispatch({ type: USER_LOADING });
axios
.get('/api/auth/user', tokenConfig(getState))
.then(res =>
dispatch({
type: USER_LOADED,
payload: res.data
})
)
.catch(err => {
dispatch(returnErrors(err.response.data, err.response.status));
dispatch({
type: AUTH_ERROR
});
});
};
//register user
export const register = ({ name, email, password }) => dispatch => {
//headers
const config = {
headers: {
'Content-Type': 'application/json'
}
};
//request body
const body = JSON.stringify({ name, email, password });
axios
.post('api/users', body, config)
.then(res =>
dispatch({
type: REGISTER_SUCCESS,
payload: res.data
})
)
.catch(err => {
dispatch(returnErrors(err.response.data, err.response.status, 'REGISTER_FAIL'));
dispatch({
type: REGISTER_FAIL
});
});
};
//setup config/headers and token
export const tokenConfig = getState => {
//get token from localstorage
const token = getState().auth.token;
//headers
const config = {
headers: {
'Content-Type': 'application/json'
}
};
//if token, add to headers
if (token) {
config.headers['x-auth-token'] = token;
}
return config;
};
users.js
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const config = require('config');
const jwt = require('jsonwebtoken');
//User Model
const User = require('../../models/User');
//#route POST api/users
router.post('/', (req, res) => {
const { name, email, password } = req.body;
//validation
if (!name || !email || !password) {
return res.status(400).json({ msg: 'Please enter all fields' });
}
//check for existing users
User.findOne({ email }).then(user => {
if (user) {
return res.status(400).json({ msg: 'User already exists!' });
}
const newUser = new User({
name,
email,
password
});
//create salt & hash
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser.save().then(user => {
jwt.sign(
{ id: user.id },
config.get('jwtSecret'),
{
expiresIn: 3600
},
(err, token) => {
if (err) throw err;
res.json({
token,
user: {
id: user.id,
name: user.name,
email: user.email
}
});
}
);
});
});
});
});
});
module.exports = router;
signup.js (component)
import React, { useState } from "react";
import Avatar from "#material-ui/core/Avatar";
import Button from "#material-ui/core/Button";
import CssBaseline from "#material-ui/core/CssBaseline";
import TextField from "#material-ui/core/TextField";
import Link from "#material-ui/core/Link";
import Grid from "#material-ui/core/Grid";
import Box from "#material-ui/core/Box";
import Typography from "#material-ui/core/Typography";
import { makeStyles } from "#material-ui/core/styles";
import Container from "#material-ui/core/Container";
import Logo from "./assets/images/logo_transparent.png";
//import Register from "./components/Register";
//redux
import { connect } from "react-redux";
//proptypes
import PropTypes from "prop-types";
import { register } from "./actions/authActions";
const useStyles = makeStyles(theme => ({
"#global": {
body: {
backgroundColor: theme.palette.common.white
}
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3)
},
submit: {
margin: theme.spacing(3, 0, 2),
background: "linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)",
border: 0,
borderRadius: 3,
boxShadow: "0 3px 5px 2px rgba(255, 105, 135, .3)",
color: "white"
},
home: {
margin: theme.spacing(0, 0, 2),
border: 0,
borderRadius: 3,
boxShadow: "0 3px 5px 2px rgba(255, 105, 135, .3)"
}
}));
function SignUp(props) {
const classes = useStyles();
const [form, setValues] = useState({
name: "",
email: "",
password: "",
msg: null
})
;
const onChange = e => {
setValues({
...form,
[e.target.name]: e.target.value,
[e.target.email]: e.target.value,
[e.target.password]: e.target.value
});
};
const handleClick = e => {
e.preventDefault();
const { name, email, password } = form;
//create user object
const newUser = {
name,
email,
password
};
//attempt to register
props.register(newUser);
alert("Registration details submitted (test)" + name + email +
password);
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar
alt="logo"
src={Logo}
style={{
width: 150,
height: 150
}}
/>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
autoComplete="name"
name="name"
variant="outlined"
required
fullWidth
id="name"
label="Full Name"
autoFocus
onChange={onChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
onChange={onChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Password"
type="password"
autoComplete="current-password"
onChange={onChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Confirm Password"
type="password"
id="password"
autoComplete="current-password"
/>
</Grid>
<Grid item xs={12} />
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={handleClick}
>
Sign Up
</Button>
<Button
href="http://localhost:3000/"
fullWidth
variant="contained"
color="primary"
className={classes.home}
>
Home
</Button>
<br />
<br />
<Grid container justify="flex-end">
<Grid item>
<Link href="http://localhost:3000/signin" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</form>
</div>
<Box mt={5}>
<MadeWithLove />
</Box>
</Container>
);
}
SignUp.propTypes = {
isAuthenticated: PropTypes.bool,
error: PropTypes.object.isRequired,
register: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
isAuthenticated: state.auth.isAuthenticated,
error: state.error //getting from reducer
});
export default connect(
mapStateToProps,
{ register } //from redux actions //mapdispatchtoprop
)(SignUp); //component
//error screenshot
I see one thing which seems wrong to do it:
you are trying to post these fields ({name, email, password}) but remove the curly brace like this: (name, email, password)
use this way
//register user
export const register = (name, email, password) => dispatch => {
//headers
const config = {
headers: {
'Content-Type': 'application/json'
}
};
//request body
const body = JSON.stringify({ name, email, password });
axios
.post('api/users', body, config)
.then(res =>
dispatch({
type: REGISTER_SUCCESS,
payload: res.data
})
)
.catch(err => {
dispatch(returnErrors(err.response.data, err.response.status, 'REGISTER_FAIL'));
dispatch({
type: REGISTER_FAIL
});
});
};
erase user: null from Auth Error action
I've been reviewing my HTTP/AJAX project and was able to implement my get, post and delete. But I tried to implement the put request on my own and have been stuck on it for two days (I know).
My understanding is that there should be the axios request in an event handler, and then you bind the handler. My put request has the id and is updating, but the id (friend.id) is only replaced by an empty string. Put request is working in the server and updates the data correctly. So I see my problem is in React.
I looked up help guides on editing state and applying it to the put request. I initialized editing: false as state, made a handler for setting editing to true and did an onChange on each input in the form for editing at the bottom. But I see that I'm not understanding how the handleUpdating event handler should connect with put (I commented them below), or if I needed it.
Here is my file hierarchy:
Here is the server's put request (located in server.js):
app.put('/friends/:id', (req, res) => {
const { id } = req.params;
let friendIndex = friends.findIndex(friend => friend.id == id);
if (friendIndex >= 0) {
friends[friendIndex] = { ...friends[friendIndex], ...req.body };
res.status(200).json(friends);
} else {
res
.status(404)
.json({ message: `The friend with id ${id} does not exist.` });
}
});
And here is the code in my React Application (located in Friendslist.js):
import React from 'react';
import axios from 'axios';
const API_URL = 'http://localhost:5000/friends';
class FriendsList extends React.Component {
constructor() {
super();
this.state = {
friends: [],
editing: false,
loading: true,
showComponent: false,
name: '',
age: '',
email: ''
}
}
componentDidMount() {
axios
.get(`${API_URL}`)
.then(response => {
console.log(response.data);
this.setState({ friends: response.data, loading: false });
})
.catch(error => {
console.log('There was an error', error);
})
}
onClickComponent = () => {
this.setState({ showComponent: true });
}
handleName = (event) => {
event.preventDefault();
this.setState({
name: event.target.value
});
}
handleAge = (event) => {
event.preventDefault();
this.setState({
age: event.target.value
});
}
handleEmail = (event) => {
event.preventDefault();
this.setState({
email: event.target.value
});
}
// handleUpdating - setting edit to true
handleUpdating = (event) => {
this.setState({ editing: true })
}
onClickComponent = () => {
this.setState({ showComponent: true });
}
handleDelete = (id) => {
axios
.delete(`${API_URL}/${id}`)
.then(response => {
this.setState({
friends: response.data
})
console.log(response.data)
})
.catch(error => {
console.log(error)
})
};
handleSubmit = (event) => {
event.preventDefault();
axios.post(`${API_URL}`, {
name: this.state.name,
age: this.state.age,
email: this.state.email
})
.then(response => {
this.setState({ friends: response.data });
})
.catch(error => {
console.log(error);
});
}
// This is the put request
handleEdit = (id) => {
axios.put(`${API_URL}/${id}`, {
name: this.state.name,
age: this.state.age,
email: this.state.email
})
.then(response => {
this.setState({ friends: response.data });
})
.catch(error => {
console.log(error);
});
}
render() {
if (this.state.loading) {
return <h1>Loading Friends....</h1>
} else if (!this.state.loading) {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.name} onChange={this.handleName} />
</label>
<label>
Age:
<input type="text" value={this.state.age} onChange={this.handleAge} />
</label>
<label>
Email:
<input type="text" value={this.state.email} onChange={this.handleEmail} />
</label>
<input type="submit" value="Submit" />
</form>
<div>{this.state.friends.map((friend) => {
return (
<div onChange={() => this.handleUpdating} key={friend.id} className="friend">
<div className="friend-name">{friend.name}</div>
<div className="friend-age">{`Age: ${friend.age}`}</div>
<div className="friend-email">{`Email: ${friend.email}`}</div>
<button onClick={() => this.handleDelete(friend.id)}>Delete</button>
<button onClick={this.onClickComponent}>Edit</button>
{this.state.showComponent ? <Form handleEdit={() => this.handleEdit(friend.id)} /> : null}
</div>
);
})}
</div>
</div>
);
}
}
}
const Form = (props) => {
return (
<form onSubmit={() => props.handleEdit(props.id)}>
<label>
Name: <input type="text" value={props.name} onChange={this.handleUpdating} />
</label>
<label>
Age: <input type="text" value={props.age} onChange={this.handleUpdating} />
</label>
<label>
Email: <input type="text" value={props.email} onChange={this.handleUpdating} />
</label>
<input type="submit" value="Update" />
</form>
);
}
export default FriendsList;
I appreciate any help and/or feedback!
enter code here
import { connect } from 'react-redux';
import api from '../../services/api';
import * as actions from '../../store/actions';
updateTool = (id) => {
console.tron.log('edit !!!');
this.setState({ isEdit: true });
const modifyTool = {
id: this.props.id,
title: this.state.title,
link: this.state.link,
description: this.state.description,
tags: this.state.tags,
};
api
.put(`/tools/${id}`, modifyTool)
.then((res) => {
console.log(res.data);
})
.catch((error) => {
console.log(error);
});
};
{/* form Modal Update */}
<section>
<Modal
visible={this.state.showModal}
width="400"
height="370"
effect="fadeInUp"
onClickAway={() => this.closeModal()}
>
<FormModal>
<form onSubmit={this.updateTool}>
<div>
<span>Update tool</span>
<label>Tool Name</label>
<input
type="text"
onChange={this.handleChange}
value={tool.title}
name="title"
/>
<label>Tool Link</label>
<input
type="text"
onChange={this.handleChange}
value={this.props.link}
name="link"
/>
<label>Tool description</label>
<textarea
cols={20}
rows={5}
name="description"
onChange={this.handleChange}
value={this.state.description}
/>
<label>Tags</label>
<input
type="text"
onChange={this.handleChange}
value={this.state.tags}
name="tags"
/>
</div>
<AlignHorizontalRight>
<button>Cancel</button>
<div>
<input
type="button"
value="EDIT"
type="submit"
onClick={() => this.updateTool(tool.id)}
/>
</div>
</AlignHorizontalRight>
</form>
</FormModal>
</Modal>
</section>
const mapDispatchToProps = dispatch => ({
removeTool: (id) => {
dispatch(actions.removeTool(id));
},
updateTool: (id, title, link, description, tags) => {
dispatch(actions.updateTool(id, title, link, description, tags));
},
});
export default connect(
null,
mapDispatchToProps,
)(Content);
actions/index.js
export const ADD_TOOL = 'ADD_TOOL';
export const REMOVE_TOOL = 'REMOVE_TOOL';
export const UPDATE_TOOL = 'UPDATE_TOOL';
const nextId = 0;
export function addTool(title, link, description, tags) {
return {
type: ADD_TOOL,
id: nextId,
title,
link,
description,
tags,
};
}
export function removeTool(id) {
return {
type: REMOVE_TOOL,
id,
};
}
export function updateTool(id, title, link, description, tags) {
return {
type: UPDATE_TOOL,
id,
title,
link,
description,
tags,
};
}
store/index.js
import { createStore } from 'redux';
const store = createStore(() => {});
export default store;
reducers/index.js
import { combineReducers } from 'redux';
import tool from './tool';
const store = combineReducers({
tool,
});
export default store;
reducers/tool.js
import { ADD_TOOL, REMOVE_TOOL, UPDATE_TOOL } from '../actions';
const INITIAL_STATE = [];
export default function Tool(state = INITIAL_STATE, action) {
switch (action.type) {
case ADD_TOOL:
return [
...state,
{
id: action.id,
title: action.title,
link: action.link,
description: action.description,
tags: action.tags,
},
];
case REMOVE_TOOL:
return state.filter(({ id }) => id !== action.id);
case UPDATE_TOOL:
return state.map(tool => (tool.id === action.id ? { ...tool, ...action } : tool));
default:
return state;
}
}
enter code here
=================================
friend Dev, here is all the data I developed to create a CRUD and with me it worked fine, I'm just having difficulty clicking a record, and this record appears in the form to edit, the main one is doing what it is, API data.
Good luck in your studies.
NOTE: I'm assuming that you already know how to use the data described above, it's not in the order, but just coding or paste correctly will work.