React: Static method always returns undefined - javascript

I have a problem with a static method in React using ESLint with airbnb config. I have a service like this that is both used for creating a user in my system, and getting all the field values for the create user form. The service looks like this:
import axios from 'axios';
import ServiceException from './ServiceException';
class CreateUserServiceException extends ServiceException {}
class CreateUserService {
constructor(config) {
this.apiUrl = config.API_URL;
this.userDomain = config.USER_DOMAIN;
}
static getFormFields() {
return [
{
id: 'username',
type: 'text',
title: 'E-postadress',
placeholder: 'Användarnamn',
mandatory: true,
extra: '',
},
{
id: 'password',
type: 'password',
title: 'Lösenord',
placeholder: 'Lösenord',
mandatory: true,
extra: '',
},
];
}
async createUser(data) {
try {
await axios.post(`${this.apiUrl}/users/${this.userDomain}`, data, { withCredentials: true });
} catch ({ response }) {
throw new CreateUserServiceException(
response.status, 'Failed to create user', response.data,
);
}
}
}
export default CreateUserService;
I also have a jsx controller to create my form. This controller gets the service via it's properties. The controller looks like this:
import React from 'react';
import './index.css';
class CreateUserController extends React.Component {
constructor(props) {
super(props);
this.state = {
formFields: [],
userData: {},
};
this.onCreate = this.onCreate.bind(this);
this.onLoad = this.onLoad.bind(this);
}
async componentDidMount() {
await this.onLoad();
}
async onLoad() {
const { createUserService } = await this.props;
const { getFormFields } = createUserService;
const formFields = getFormFields || []; // ALWAYS RETURNS UNDEFINED
const userData = {};
console.log(formFields); // ALWAYS DISPLAYS []
formFields.forEach((field) => {
userData[field.id] = '';
});
this.setState({ formFields, userData });
}
async onCreate(e) {
e.preventDefault();
const { userData } = this.state;
console.log(userData);
}
render() {
const { userData, formFields } = this.state;
return (
<section className="create-user-controller">
<h1>Skapa ny användare</h1>
<form
className="centered-container"
action=""
noValidate
onSubmit={this.onCreate}
>
<table>
<tbody>
{formFields.map(field => (
<tr key={field.id}>
<td>{field.title}</td>
<td>
<input
value={userData[field.id]}
onChange={e => this.setState({
userData: { ...userData, [field.id]: e.target.value },
})}
className={`create-${field.id}`}
name={field.id}
placeholder={field.placeholder}
type={field.type}
/>
</td>
<td>{field.extra}</td>
</tr>
))}
<tr>
<td colSpan={3}>* obligatoriskt</td>
</tr>
</tbody>
</table>
<input type="submit" className="btn btn-green" value="Skapa användare" />
</form>
</section>
);
}
}
export default CreateUserController;
My problem is that const formFields = getFormFields || []; always becomes [] which means that getFormFields always returns undefined.
If I remove static from getFormFields() in my service and call it using const formFields = createUserService.getFormFields(); it works fine, but then ESLint complains about ESLint: Expected 'this' to be used by class method 'getFormFields'. (class-methods-use-this).
Does anyone have an idea how to solve this?

import CreateUserService from './CreateUserService';
...
async onLoad() {
...
const formFields = CreateUserService.getFormFields() || [];
...
}
Should do the trick !
Notice that the static function is called using the Class name. Tou will also have to import it correctly (i don't know your path…)

Related

In React, can a Parent class access fields from a Child class?

I am learning React. In a tutorial I saw online regarding handling Form submissions, there is a component Form which is being extended by another component LoginForm.
Form.jsx:
import { React, Component } from "react";
import Joi from "joi-browser";
class Form extends Component {
state = {
data: {},
errors: {},
};
validate = () => {
const options = { abortEarly: false };
const { error } = Joi.validate(this.state.data, this.schema, options);
if (!error) return null;
const errors = {};
for (let item of error.details) errors[item.path[0]] = item.message;
return errors;
};
validateProperty = ({ name, value }) => {
const obj = { [name]: value };
const schema = { [name]: this.schema[name] };
const { error } = Joi.validate(obj, schema);
return error ? error.details[0].message : null;
};
handleSubmit = (e) => {
e.preventDefault();
const errors = this.validate();
this.setState({ errors: errors || {} });
if (errors) return;
this.doSubmit();
};
handleChange = ({ currentTarget: input }) => {
const errors = { ...this.state.errors };
const errorMessage = this.validateProperty(input);
if (errorMessage) errors[input.name] = errorMessage;
else delete errors[input.name];
const data = { ...this.state.data };
data[input.name] = input.value;
this.setState({ data, errors });
};
}
export default Form;
LoginForm.jsx:
import React, { Component } from "react";
import Form from "./common/form";
import Joi from "joi-browser";
import Input from "./common/input";
class LoginForm extends Form {
state = {
data: { username: "", password: "" },
errors: {},
};
schema = {
username: Joi.string().required().label("Username"),
password: Joi.string().required().label("Password"),
};
doSubmit = () => {
// call the server
console.log("Submitted");
};
render() {
const { data, errors } = this.state;
return (
<div>
<h1>Login</h1>
<form onSubmit={this.handleSubmit}>
<Input
name="username"
value={data.username}
label="Username"
onChange={this.handleChange}
error={errors.username}
/>
<Input
name="password"
value={data.password}
label="Password"
onChange={this.handleChange}
error={errors.password}
/>
<button disabled={this.validate()} className="btn btn-primary">
Login
</button>
</form>
</div>
);
}
}
export default LoginForm;
Since LoginForm is extending Form, in the validate() function, how can Form be using properties like this.schema, if schema is defined in LoginForm.jsx?

How to show submitted data in React immediately, without refreshing the page?

I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).

Why is my record not updating in my ASP.NET CORE React Redux Application?

I'm new to React with Redux and I've been working on a new web application that has some basic crud operations. I am building this using ASP.NET Core with a repository pattern.
My application is showing data correctly and I can also add data correctly, the problem I have is that updating my data isn't working. Whilst the data is passed into the controller, you can see the changes sitting in the parameter, once I try to commit the data it doesn't update.
My project is set up as follows, I've shortened certain parts it to only include the component I'm working with.
Shelly.Data
|-BaseEntity.cs
|-Vessel.cs
Shelly.Repo
|-IRepository.cs
|-Repository.cs
|-ShellyContext.cs
Shelly.Services
|-IVesselService.cs
|-VesselService.cs
Shelly.UI
|-ClientApp
|-src
|-components
|-vessels
|-VesselsComponent.js
|-store
|-Vessels.js
I've included the code from my repository in this question as I'm not convinced the issue is with my React setup but perhaps someone can help me with that.
Repo/IRepository.cs
public interface IRepository<TEntity> where TEntity : BaseEntity
{
IEnumerable<TEntity> GetAll();
TEntity Get(long id);
void Insert(TEntity entity);
void Update(TEntity entity);
void Delete(TEntity entity);
void Remove(TEntity entity);
void SaveChanges();
}
Repo/Repository.cs
public class Repository<TEntity> : IRepository<TEntity> where TEntity : BaseEntity
{
private readonly ShellyContext _dbContext;
private DbSet<TEntity> entities;
string errorMessage = string.Empty;
public Repository(ShellyContext context)
{
this._dbContext = context;
entities = context.Set<TEntity>();
}
...
public void Update(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
_dbContext.SaveChanges();
}
public void SaveChanges()
{
_dbContext.SaveChanges();
}
...
}
Services/IVesselService
public interface IVesselService
{
IEnumerable<Vessel> GetVessels();
Vessel GetVessel(long id);
void InsertVessel(Vessel vessel);
void UpdateVessel(Vessel vessel);
void DeleteVessel(long id);
}
Services/VesselService
public class VesselService : IVesselService
{
private IRepository<Vessel> vesselRepository;
public VesselService(IRepository<Vessel> vesselRepository)
{
this.vesselRepository = vesselRepository;
}
public void UpdateVessel(Vessel vessel)
{
vesselRepository.Update(vessel);
}
}
The next part is the controller which is called from react to carry out the CRUD operations and also serve up the data to the API. Reading and Added seem to work but Updating isn't, you can see the updated data being passed in vessel but it doesn't seem to commit and just refreshes with the old data.
Controllers/VesselDataController.cs
[Route("api/[controller]")]
public class VesselDataController : Controller
{
private readonly IVesselService vesselService;
public VesselDataController(IVesselService vesselService)
{
this.vesselService = vesselService;
}
...
[HttpPost]
public ActionResult AddVessel([FromBody]Vessel vessel)
{
vesselService.InsertVessel(vessel);
return Ok(new
{
success = true,
returncode = "200"
});
}
[HttpPut]
public ActionResult Update([FromBody]Vessel vessel)
{
vesselService.UpdateVessel(vessel);
return Ok(new
{
success = true,
returncode = "200"
});
}
}
Here is the code for my React/Redux configuration. Again, I've only included code for my relative component.
ClientApp/src/components/VesselsComponent.js
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { actionCreators } from '../../store/Vessels';
class VesselsComponent extends Component {
state = {
name: "",
imo: "",
editing: ""
};
componentWillMount() {
this.props.requestVessels();
}
toggleEditing(itemId) {
console.log("Editing" + ' ' + itemId);
this.setState({ editing: itemId });
}
handleVesselUpdate(vessel) {
this.props.updateVessel(vessel);
setTimeout(this.props.requestVessels, 600);
}
handleEditItem() {
let itemId = this.state.editing;
var editVessel = this.props.vessels.find((v) => v.Id === itemId);
editVessel.IMO = this.refs[`IMO_${itemId}`].value;
editVessel.AddedDate = this.refs[`AddedDate_${itemId}`].value;
editVessel.ModifiedDate = this.refs[`ModifiedDate_${itemId}`].value;
this.handleVesselUpdate(editVessel);
this.setState({ editing: "" });
}
renderItemOrEditField(vessel) {
if (this.state.editing === vessel.Id) {
return (
<tr key={vessel.Id}>
<td>{vessel.Name}</td>
<td>{vessel.IMO}</td>
<td>
<input onKeyDown={this.handleEditField} type="text" ref={`IMO_${vessel.Id}`} name="IMO" defaultValue={vessel.IMO} />
<input onKeyDown={this.handleEditField} type="text" ref={`AddedDate_${vessel.Id}`} name="AddedDate" defaultValue={vessel.AddedDate} />
<input onKeyDown={this.handleEditField} type="text" ref={`ModifiedDate_${vessel.Id}`} name="ModifiedDate" defaultValue={vessel.ModifiedDate} />
</td>
<td>
<button onClick={this.handleEditItem.bind(this)} label="Update Item">Update</button>
</td>
</tr>
)
} else {
return (
<tr key={vessel.Id}>
<td>{vessel.Name}</td>
<td>{vessel.IMO}</td>
<td><button onClick={this.toggleEditing.bind(this, vessel.Id)} className="btn btn-info">Edit</button></td>
</tr>);
}
}
renderVesselsTable(props) {
return (
<table className="table">
<thead className="thead-dark">
<tr>
<th>Name</th>
<th>IMO</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{props.vessels.map(vessel =>
this.renderItemOrEditField(vessel)
)}
</tbody>
</table>
)
}
render() {
return (
<div>
<h3>Vessels</h3>
{this.renderVesselsTable(this.props)}
<table className="table">
<thead className="thead-dark">
</thead>
<tbody>
<tr>
<td>Name:</td>
<td>
<input className="form-control" id="vesselName" type="text" value={this.state.name} onChange={(ev) => this.setState({ name: ev.target.value })} />
</td>
</tr>
<tr>
<td>IMO:</td>
<td>
<input className="form-control" id="vesselImo" type="text" value={this.state.imo} onChange={(ev) => this.setState({ imo: ev.target.value })} />
</td>
</tr>
<tr>
<td>
<button className="btn btn-default btn-success" onClick={this.addVessel.bind(this)}>Add Vessel</button>
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
export default connect(
state => state.vessels,
dispatch => bindActionCreators(actionCreators, dispatch)
)(VesselsComponent);
Finally, here is the Vessel.js from the store.
const requestVesselsType = 'REQUEST_VESSELS';
const receiveVesselsType = 'RECEIVE_VESSELS';
const requestVesselType = 'REQUEST_VESSEL';
const receiveVesselType = 'RECEIVE_VESSEL';
const addVesselType = 'ADD_VESSEL';
const updateVesselType = "UPDATE_VESSEL";
const initialState = { vessels: [], vessel: {}, isLoading: false };
let currentvessel = {};
export const actionCreators = {
requestVessels: () => async (dispatch, getState) => {
dispatch({ type: requestVesselsType });
const url = 'api/VesselData/GetVessels';
const response = await fetch(url);
const allvessels = await response.json();
dispatch({ type: receiveVesselsType, allvessels });
},
requestVessel: () => async (dispatch, getState) => {
dispatch({ type: requestVesselType });
const url = 'api/VesselData/GetVessel/${id}';
const response = await fetch(url);
const vessel = await response.json();
dispatch({ type: receiveVesselType, vessel });
},
updateVessel: (vessel) => async (dispatch, getState) => {
const baseURL = "/api/VesselData";
const data = JSON.stringify({
Id: vessel.Id,
Name: vessel.Name,
IMO: vessel.IMO,
ModifiedDate: vessel.ModifiedDate,
AddedDate: vessel.AddedDate
});
const fetchTask = fetch(baseURL, {
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type" : "application/json",
},
body: data
})
.then((data => {
dispatch({ type: updateVesselType, vessel: data })
}))
}
}
export const reducer = (state, action) => {
state = state || initialState;
if (action.type === requestVesselsType) {
return {
...state,
isLoading: true
};
}
if (action.type === receiveVesselsType) {
return {
...state,
vessels: action.allvessels,
isLoading: false
}
}
if (action.type === requestVesselType) {
return {
...state,
isLoading: true
};
}
if (action.type === receiveVesselType) {
currentvessel = action.vessel;
return {
...state,
vessel: currentvessel,
isLoading: false
}
}
if (action.type === updateVesselType) {
return {
...state,
isLoading: false
};
}
return state;
};
So, that's my application, it's basic and I'm still learning as I go but I can't see any logical reason for the lack of commit from the update method. The saving of the context is handled in the repository and I know it hits it and no record updates. Can anyone help me understand where I've gone wrong?
If your question contains complete code, I believe the problem is in your Repository update method. It's not doing anything.
public void Update(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
_dbContext.SaveChanges();
}
You need to attach the object you want to update into the DbContext. You can do that with a DbContext.Update method
Try to call Update before SaveChanges, like this
public void Update(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
_dbContext.Update(entity); //add this line
_dbContext.SaveChanges();
}

MobX: getting data into the store from another store

I use this construction for form store and validate: https://medium.com/#KozhukharenkoN/react-form-validation-with-mobx-8ce00233ae27
There is a store form:
import { observable, action, computed } from 'mobx'
import FormStore from './FormStore'
import UserStore from 'stores/UserStore'
class SettingsFormStore extends FormStore {
#observable
form = {
fields: {
email: {
value: UserStore.email,
defaultValue: UserStore.email,
error: null,
rule: 'required|email'
},
},
meta: {
isValid: true,
error: null,
},
}
}
export default new SettingsFormStore()
there is a stor user:
import { observable, action, computed } from 'mobx'
import * as UserAPI from 'api/UserAPI'
class UserStore {
#observable id
#observable email
constructor() {
this.load()
}
#action setValues(values) {
this.id = values.id
this.email = values.email
}
#action removeValues() {
this.id = null
this.email = null
}
load() {
UserAPI.getMe()
.then(result => {
this.setValues(result.user)
})
}
}
export default new UserStore()
In the form component I get email from store:
const email = SettingsFormStore.form.fields.email.value
but email some reason undefied, although UserStore.email keeps the value...
reaction(
() => ({ email: UserStore.email, id: UserStore.id }),
({ email, id }) => {
this.form.fields.email.value = email
...
}
)
I found a solution:
constructor() {
super()
reaction(
() => UserStore.email,
email => {
this.form.fields.email.value = email
}
)
}

React-Flux Load initial state

I'm trying to make an Ajax request in al React Flux app with axios and I get data after state is set.
I have this code in root app:
InitialData.getInitialPosts();
The API request it looks like this:
let PostsApi = {
getAllPosts(){
return axios.get('https://jsonplaceholder.typicode.com/posts')
.then( (response) => {
console.log('All posts: ', response.data)
return response.data;
});
}
}
export default PostsApi;
In actions/initialData.js i have this:
let LoadInitialData = {
getInitialPosts(){
Dispatcher.dispatch({
actionType: 'LOAD_INITIAL_POSTS',
initialPosts: {
posts: PostsApi.getAllPosts()
}
})
}
}
export default LoadInitialData;
In store:
let _posts = [];
const PostsStore = Object.assign({}, EventEmitter.prototype, {
addChangeListener(callback){
this.on('change', callback)
},
removeChangeListener(callback){
this.removeChangeListener('change', callback)
},
emitChange(callback){
this.emit('change', callback)
},
getAllPosts(){
return _posts;
}
});
Dispatcher.register(function(action){
switch(action.actionType){
case 'LOAD_INITIAL_POSTS':
_posts = action.initialPosts.posts;
PostsStore.emitChange();
break;
default:
}
});
In View:
export default class PostsPage extends React.Component {
constructor(){
super();
this.state = {
posts: []
}
}
componentDidMount(){
this.setState({
posts: PostsStore.getAllPosts()
});
}
render(){
const { posts } = this.state;
return(
<div>
{posts.map( post => {
return <h3 key={post.id}>{post.title}</h3>
})}
</div>
)
}
}
On console.log:
state: Object {posts: Array[0]}
state: Object {posts: Promise}
postsApi.js:7 All posts: [Object, Object, Object, Object, Object, Object...]
And the problem is the ajax request is after componentDidMount.
Your PostsPage component is not set up correctly to listen to changes from the store. The code you have will only grab the list of posts once when it first mounts. You want it to update whenever the Store gets new data.
To accomplish this, you need to utilize the add/remove Change Listener functions that you setup in the Store. It should look something like this;
export default class PostsPage extends React.Component {
constructor(){
super();
this.state = {
posts: []
}
}
_calculateState(){
this.setState({
posts: PostsStore.getAllPosts()
});
}
componentDidMount(){
PostsStore.addChangeListener(this._calculateState);
},
componentWillUnmount(){
PostsStore.removeChangeListener(this._calculateState);
},
render(){
const { posts } = this.state.posts;
return(
<div>
{posts.map( post => {
return <h3 key={post.id}>{post.title}</h3>
})}
</div>
)
}
}

Categories

Resources