I'm working on a webapp for Django. It is a simple blog. The front end is being built in react and materials UI, it is based off of the tutorial here, but I have had to adapt much by myself because he is using a Class-oriented approach many methods of which are deprecated as React seems to be moving towards a function based model. In the model that the demonstrator created, we are passing data from our react frontend to our django backend by use of generating JSON objects which python serializes. I have built a blog post submission form that doesn't work for a reason I can't understand.
CreateBlogPost.js
import React, { Component } from "react";
import Grid from "@mui/material/Grid";
import { FormControl, FormHelperText, TextField, Typography, Button } from "@mui/material";
import { Link } from "react-router-dom";
import { useState } from "react";
import { Navigate } from "react-router-dom";
export default function CreateBlogPost(props) {
const[title,setTitle] = useState('');
const[content,setContent] = useState('');
const[tags, setTags] = useState('');
const handleTitle = event => {
setTitle(event.target.value);
}
const handleContent = event => {
setContent(event.target.value);
}
const handleTags = event => {
setTags(event.target.value);
}
const requestOptions = {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: title,
content: content,
tags: tags,
})
};
const handlePostButtonPressed = (requestOptions) => {
console.log(requestOptions)
fetch("/blog/create",requestOptions)
.then((response) => response.json())
.then((data) => Navigate('/posts/' data.postid));
}
return (
<Grid container spacing={1} className="pl-2 pt-4">
<Grid item xs={12}>
<Typography component="h4" variant="h4">Create a post</Typography>
<Grid item xs={12} className="-ml-2 mt-4">
<FormHelperText>
<div>Title:</div>
</FormHelperText>
<FormControl>
<TextField required={true} type="text" value={title} onChange={handleTitle}/>
</FormControl>
</Grid>
</Grid>
<Grid item xs={12} className="-ml-2 mt-4">
<FormHelperText>
<div>Content:</div>
</FormHelperText>
<FormControl component="fieldset">
<TextField required={true} type="text" value={content} onChange={handleContent}/>
</FormControl>
</Grid>
<Grid item xs={12} className="-ml-2 mt-4">
<FormHelperText>
<div>Tags:</div>
</FormHelperText>
<FormControl component="fieldset">
<TextField required={true} type="text" value={tags} onChange={handleTags}/>
</FormControl>
</Grid>
<Grid item xs={12}>
<Button color="primary" variant="contained" onClick={handlePostButtonPressed}>
Post
</Button>
<Button color="secondary" variant="contained" to="/blogs/" component={Link}>
Back
</Button>
</Grid>
</Grid>
)
}
Everything in this works fine until I press the Post button. In the log I see the JSON object of the post I want to submit properly rendered (so I know this is not an error with any of the data handler functions), but I get an error where the fetch function occurs:
"Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- You might have more than one copy of React in the same app"
I do not believe I have mismatching versions of React. Here is my package.json:
{
"name": "frontend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack watch --mode development ",
"build": "webpack --mode production"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.7",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"babel-loader": "^9.1.0",
"glob-all": "^3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.2.4",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.11.0",
"@mui/material": "^5.11.3",
"autoprefixer": "^10.4.13",
"css-loader": "^6.7.3",
"draft-js": "^0.11.7",
"postcss-cli": "^10.1.0",
"postcss-loader": "^7.0.2",
"react-draft-wysiwyg": "^1.15.0",
"react-router-dom": "^6.6.1",
"style-loader": "^3.3.1"
}
}
I have researched this problem and have not been able to find a solution. I have found other ways of building forms in react, but that would change how my backend receives the data. I would prefer to avoid that unless absolutely necessary.
CodePudding user response:
Issue
The code imports the Navigate
component and tries to directly invoke it.
import { Navigate } from "react-router-dom";
export default function CreateBlogPost(props) {
...
const handlePostButtonPressed = (requestOptions) => {
console.log(requestOptions)
fetch("/blog/create", requestOptions)
.then((response) => response.json())
.then((data) => Navigate('/posts/' data.postid)); // <-- invoked here
};
...
In React we don't directly call/invoke React components, we render them as JSX in the render return of a React component, i.e. <Navigate to="..." />
. By directly calling the React component as a function you are breaking the rules of hooks (indirectly).
Navigate
is a component that, when rendered, effects a declarative navigate action, i.e. navigates to the specified route path. I'm certain you meant to import the useNavigate
hook to use the navigate
function and effect an imperative navigation action in the handler.
Solution
Import useNavigate
and issue an imperative navigate action. Remove the requestOptions
arg and use the requestOptions
that is declared in function component scope. This is because handlePostButtonPressed
is passed directly as an event handler.
import { useNavigate } from "react-router-dom";
export default function CreateBlogPost(props) {
const navigate = useNavigate();
...
const requestOptions = {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: title,
content: content,
tags: tags,
})
};
const handlePostButtonPressed = () => {
console.log(requestOptions)
fetch("/blog/create", requestOptions)
.then((response) => response.json())
.then((data) => navigate(`/posts/${data.postid}`);
};
...
<Button
color="primary"
variant="contained"
onClick={handlePostButtonPressed}
>
Post
</Button>
...