React setState not setting property to File value - javascript

I'm trying to create a custom useForm hook which will update an object and handle multiple input types. When I upload an image, I grab the first image and save it to value BUT when I setState with setInputs, the image value in the inputs object is null even though the value is the File object (as I've console logged it before).
I'm not sure if this is a typescript specific error. I tried setting profileImage to be any without any impact.
EDIT: The image is being uploaded correctly, but not displaying when using JSON.stringify as mentioned in the comments... so no actual error.
// Hook usage
const { inputs, handleInput } = useForm<{
name: string
description: string
profileImage: File | null
}>({
name: "",
description: "",
profileImage: null,
})
// Form Input
<input
onChange={handleInput}
type="file"
accept="image/*"
id="profileImage"
name="profileImage"
/>
// useForm hook
export type FormEvent = React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLTextAreaElement>
export default function useForm<T extends { [key: string]: any }>(initial: T = {} as T) {
const [inputs, setInputs] = useState<T>(initial)
const handleInput = (e: FormEvent) => {
let { value, name, type }: { value: any; name: string; type: string } = e.target
if (type === "number") {
value = parseInt(value)
} else if (type === "file") {
value = (e.target as HTMLInputElement).files![0]
}
setInputs({ ...inputs, [name]: value })
}
return { inputs, setInputs, handleInput }
}

I reproduced a simplified version of your example, just handling the file case. The upload onChange action is handled by the hook, file object is returned and name gets displayed on the HTML.
https://codesandbox.io/s/condescending-mcclintock-qs3c1?file=/src/App.js
function useForm(init) {
const [file, setFile] = React.useState(init);
const handleFileInput = (e) => {
setFile(e.target.files[0]);
};
return {file, handleFileInput};
}
export default function App() {
const {file, handleFileInput } = useForm(null);
console.log('file', file);
return (
<div className="App">
<h3>File Upload Test</h3>
<input type="file" onChange={handleFileInput} />
<div> File Info: {file && file.name} </div>
</div>
);
}

Related

Why is action not updating my component in this todo list?

From the extent of my knowledge, action in mobx is supposed to cause the observer to rerender, right? However, even though I'm invoking action on the handleSubmit method in my AddTask component, it does not rerender the observer(TaskList). Do I have to wrap AddTask in an observable as well? But when I tried that, it didn't render any of the data at all. I'm genuinely perplexed and have tried so many different things for hours. Please help.
AddTask:
export default function AddTask() {
const [task, setTask] = useState('');
const handleSubmit = action(async (event: any) => {
event.preventDefault();
try {
const response = await axios.post('http://localhost:5000/test', { task });
} catch (error: Error | any) {
console.log(error);
}
});
const onChange = (e: any) => {
const value = e.target.value;
if (value === null || value === undefined || value === '') {
return;
}
setTask(value);
};
return (
<div className="task">
<form onSubmit={handleSubmit}>
<input type="text" name="task" value={task} onChange={onChange}></input>
<br></br>
<input type="submit" name="submit" value="Submit" />
</form>
</div>
);
}
TaskList:
const TaskList = () => {
const [update, setUpdate] = useState<string>('');
useEffect(() => {
TaskStore.fetchTasks();
}, []);
const onChangeValue = (e: any) => {
setUpdate(e.target.value);
};
return (
<div>
<p>
update input <input onChange={onChangeValue} value={update} />
</p>
{TaskStore.tasks.map((value: any, key) => {
console.log(value);
return (
<div key={key}>
<p>
{value.task}
<DeleteTask value={value} taskList={TaskStore} />
<UpdateTask value={update} current={value} taskList={TaskStore} />
</p>
</div>
);
})}
</div>
);
};
export default observer(TaskList);
taskStore:
interface Task {
task: string;
}
class TaskStore {
constructor() {
makeAutoObservable(this);
}
tasks = [] as Task[];
#action fetchTasks = async () => {
try {
const response: any = await axios.get('http://localhost:5000/test');
this.tasks.push(...response.data.recordset);
} catch (error) {
console.error(error);
}
};
}
export default new TaskStore();
As far as I can see you are not doing anything in your submit action to handle new task.
You are making request to create new task, but then nothing, you don't do anything to actually add this task to your store on the client nor make store refetch tasks.
const handleSubmit = action(async (event: any) => {
event.preventDefault();
try {
// Request to create new task, all good
const response = await axios.post('http://localhost:5000/test', { task
// Now you need to do something
// 1) Either just add task to the store manually
// Add some action to the store to handle just one task
TaskStore.addTask(response.data)
// 2) Or you can do lazy thing and just make store refetch everything :)
// Don't forget to adjust this action to clear old tasks first
TaskStore.fetchTasks()
});
} catch (error: Error | any) {
console.log(error);
}
});
P.S. your action decorators are a bit useless, because you cannot use actions like that for async function, for more info you can read my other answer here MobX: Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed

how to pass only those values which changed in POST request body

I have multiple state variables, that contains data entered in a form by the user. Since this form is only meant to update the existing values, I have to pass in only those values that have changed from its initial value (the one returned from the GET request).
State:
const [name, setName] = useState(props.user?.name ?? null);
const [lang, setLang] = useState(props.user?.lang ?? null);
const [enableChecks, setEnableChecks] = useState(props.user?.checkEnabled ?? false)
In the event that the user only changed the name, how can I pass in only name in the request body?
What I have tried: I have the user props, so I have multiple if statements that check if the props matches the state. If it doesn't, then I add it to the request payload. This works, but when there's a lot of state, there will be a lot of if statements, which isn't nice to look at.
Is there a better way to do this?
Instead of having multiple state variables, you can have a single state variable like
const [state, setState] = useState(props.user)
and then change handler should look like
const handleChange = (e) => {
setState({
...state,
[e.target.name]: e.target.value,
});
};
finally, when submitting the form you can make your body data for post request like
const handleSubmit = () => {
const requestData = {}
for (const [key, value] of Object.entries(state)){
if(props.user[key] !== value) {
requestData[key] = value
}
}
axios.post('some api url', responseData)
}
You can keep your state in an object, and then only update field state when the updatedUser and user state values are different.
//use `import` in your real component instead
//import { useState } from 'react';
const { useState } = React;
//fake your user prop
const userProp = {
name: "Name",
lang: "English",
}
function App(props) {
const [user, setUser] = useState(props.user);
const [updatedUser, setUpdatedUser] = useState({});
const handleChange = (e) => {
const newlyUpdatedUser = {
...updatedUser,
}
if(props.user[e.target.name] === e.target.value) {
delete newlyUpdatedUser[e.target.name]
} else {
newlyUpdatedUser[e.target.name] = e.target.value
}
setUpdatedUser(newlyUpdatedUser);
setUser({
...user,
[e.target.name]: e.target.value
})
};
console.log(updatedUser)
return (
<React.Fragment>
<label>
Name:
<input value={user.name} name="name" onChange={handleChange} />
</label>
<label>
Lang:
<input value={user.lang} name="lang" onChange={handleChange} />
</label>
</React.Fragment>
);
}
ReactDOM.render(<App user={userProp} />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Keeping state of input breaks app when value is undefined

I am trying to get a form to work, which acts both as a creation- and an update-form for an item. I keep track of an edit state to decide what text should be rendered in the form and what API call to make on submitting it. So far so good, that all works well. In order to keep track of the input values, I use a hook useInputState to update the corresponding state value:
useInputState.js
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (e) => {
if (e.target) {
setValue(e.target.value);
} else { // to be able to set state manually
setValue(e);
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};
const [newTitle, setNewTitle, resetNewTitle] = useInputState();
form.js
export default function Form({
newTitle,
setNewTitle,
edit, // true or false
}) {
return (
<div>
<h2>{edit ? "Edit Item" : "Add new item"}</h2>
<div>
<label>Title</label>
<input
type="text"
placeholder="What is your title?"
value={newTitle}
onChange={setNewTitle}
/>
</div>
)
}
Now when the user adds a new item, obviously the form is empty to begin with. However, when the user wants to edit an item, I try to prepopulate the form with the information of the given object. This is where I run into issues: The object model has some optional fields. When the user leaves these blank when creating the object, editing it breaks the application with an error of can't read property target of null.
I use the following code to prepoulate the form fields when edit is toggled on:
useEffect(() => {
if (edit && selectedItem) {
setNewTitle(selectedItem.title);
}
}, [edit]);
I see why it's running into issues and I tried a lot of things like changing the setNewTitle argument to setNewTitle(selectedItem["title"] !== undefined ? selectedItem.title : ""); and similar approaches but nothing has worked so far.
What can I do to solve this issue?
It seems that you are only comparing selectedItem.title !== undefined which can result in a specified error if your selectedItem.title is equal to null;
There are multiple ways to solve the issue but you can do the following:
const handleChange = (e) => {
if (!!e && e.target) {
setValue(e.target.value);
} else { // to be able to set state manually
setValue(e);
}
};
The change is in the line: if (!!e && e.target)
In your useInputState hook, you assume that handleChange function is a React.ChangeEvent meaning it is passed with e : Event from which you read e.target.value. This breaks when you try to set the value directly here in this line, in useEffect:
setNewTitle(selectedItem.title);
I would suggest a change like this to your useInputState hook,
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (value) => {
if (value) {
setValue(value);
} else {
setValue("");
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};
then i your form do this,
export default function Form({
newTitle,
setNewTitle,
edit, // true or false
}) {
return (
<div>
<h2>{edit ? "Edit Item" : "Add new item"}</h2>
<div>
<label>Title</label>
<input
type="text"
placeholder="What is your title?"
value={newTitle}
onChange={(e)=>setNewTitle(e.target.value)}
/>
</div>
)
}
It should work as you expect it to work with this change.
Your error is can't read property target of null.
So the e arg is falsy sometimes.
Try
setNewTitle(selectedItem.title ? selectedItem.title : '')
and in the custom hook:
export default (initialVal) => {
const [value, setValue] = useState(initialVal);
const handleChange = (e) => {
if(typeof e === 'string') {
setValue(e);
} else if(e && e.target) {
setValue(e.target.value);
}
};
const reset = () => {
setValue("");
};
return [value, handleChange, reset];
};

React - Access to an object property which is settled in useEffect hook

I'm trying to access a property of an object like this one:
export const ListingDef = {
assistant: {
title: 'a'
},
contract: {
title: 'b'
},
child: {
title: 'c'
}}
I'm using Context and I have a 'listingType' property (empty string) in my state and I define its value in a useEffect hook:
const Listing = ({type}) => {
const {listingType, setListingType} = useContext(ListingContext);
useEffect(() => {
setListingType(type);
}, [])
return (
<>
<h1 className="listing-title">{ListingDef[listingType].title}</h1>
<ListingTable/>
</>
)}
That way, I can access to ListingDef[listingType].title. But I'm facing this issue:
Cannot read property 'title' of undefined
At first render, listingType is equal to empty string so I think I understand why I get this error back.
In other components, I would access to other properties of my object also based on listingType value.
It works if I deal with boolean condition, but is there anyway to avoid if(listingType)... each time I want to access to my object.
(It works fine if I use type directly from the props but I need this value in other components)
Thank you :)
My Context file:
const initialState = {
listingType: '',
listingData: [],
loading: true
}
export const ListingContext = createContext(initialState);
export const ListingProvider = ({children}) => {
const [state, dispatch] = useReducer(ListingReducer, initialState);
const setListingType = (type) => {
dispatch({
type: SET_LISTING_TYPE,
payload: type
})
}
const getListingData = async (listingType) => {
try {
const data = await listObjects(listingType);
dispatch({
type: GET_LISTING_DATA,
payload: data
})
} catch (err) {
}
}
const contextValue = {
setListingType,
getListingData,
listingType: state.listingType,
listingData: state.listingData,
loading: state.loading
}
return (
<ListingContext.Provider value={contextValue}>
{children}
</ListingContext.Provider>
)
}
I am relatively new to redux but i don't see the point of using useEffect when the prop type is already working. Either use a default value or add the 'type' prop to the useEffect second param.

File Input doesn't store file information, just the name

I'm using redux-form#8.1.0 and redux#4.0.1 and saving the data in a MongoDB collection. However, when I watch the file object that I uploaded in my Mongo DBs' collection it just retrieves the name of the file.
FileInput.js is the component that I pass to the redux-form Field component
FileInput.js
import React from 'react';
const handleChange = (handler) => ({target: {files}}) =>
handler(files.length ? {file: files[0], name: files[0].name} : {});
export default ({
input: {onChange, onBlur, value: omitValue, ...inputProps},
meta: omitMeta,
...props
}) => (
<input type="file"
onChange={handleChange(onChange)} onBlur={handleChange(onBlur)}
{...inputProps} {...props} />
);
And this is how I use it in my form
...
import FileInput from './FileInput';
...
<Field name="fileUploaded" component={FileInput} type="file"
/>
And this is the document in the MongoDB collection
{...
"fileUploaded":{"name":"testingfile.png"},
...}
It seems it stores only the name of the file and I expect another key value pair with the file information/object in order to load and display this image/file later.
Redux-Form stores the file, perhaps you need to read it out and then send it as a multipart/form-data. The data should be accessible at state.form.myFormNameHere.values.mFieldNameHere.
I made an ImageDisplay component that may be of help. It reads the file from a red-form file input and displays a preview.
const ImageReader = ({ file }) => {
const reader = new FileReader();
const [imageUrl, setImageUrl] = useState('');
if (file && file.file instanceof Blob) {
reader.onload = (event) => {
const { target: { result } } = event;
setImageUrl(result);
};
reader.readAsDataURL(file.file);
return <Image src={imageUrl} />;
}
return <Image src={null} />;
};
ImageReader.defaultProps = {
file: null,
};
ImageReader.propTypes = {
file: PropTypes.shape({
name: PropTypes.string,
file: PropTypes.any,
}),
};

Categories