Rendering new list item after adding it from a nested form. React hooks, redux, React Router V6 - ruby-on-rails

I am creating a list tracking app with React hooks, Redux, and Ruby on Rails. There is a List model, with a title as a string and completed as a boolean, and a ListItem model with descriptions as a string (the list item), completed boolean, and list_id as an integer.
I am using react route V6 for this and getting a little lost in re-rendering/ updating the page. Here is the breakdown of the application:
On the home screen, you can click to view all Lists and add a new list. when viewing all list each list title is displayed as a link to that list show page. The show page shows the list title, list items and a form to add another list item. Now where I am having trouble is being able to add a new list item, and it display on the page right after submission. Right now when I add a new item, and refresh the page it is not there. But if I click back to view all lists, then click that list again it shows up under the list items.
I tried using useNavigate to navigate to that list show page even though it is already on it but I am getting this error
Uncaught TypeError: Cannot destructure property 'list' of 'location.state' as it is null.
Here is all my components:
App.js
class App extends React.Component {
render(){
return (
<div className="App">
<Navbar/>
<br></br>
<Routes>
<Route path="/" element={<Home/>} />
<Route path="/lists" element={<Lists />} />
<Route path="/lists/new" element={<ListForm />} />
<Route path="/lists/:id" element={<ListContainer />} />
</Routes>
</div>
);
}
}
Lists.js
export default function Lists() {
const lists = useSelector(state => state.lists)
// replaces mapStateToProps
const dispatch = useDispatch()
// replaces mapDispatchToProps
useEffect(() => {
dispatch(fetchLists())
}, [])
return (
<div>
{Array.isArray(lists) && lists.map((list) => {
return (
<Link
key={list.id}
to={`/lists/${list.id}`}
state={{ list: list }}
>
<h2>{list.title}</h2>
</Link>
)
})}
</div>
)
}
ListContainer.js
export default function ListContainer() {
const location = useLocation();
const { list } = location.state;
console.log(list)
return (
<div>
<List list={list}/>
<ListItemForm list={list}/>
</div>
);
}
List.js
export default function List({list}) {
return (
<div>
<h4>{list.title}</h4>
{list.list_items.map((item) => {
return (
<div key={item.id}>
<li key={item.id}>{item.description}</li>
</div>
);
})}
<br></br>
</div>
);
}
and ListItemForm.js
export default function ListItemForm({list}) {
const [item, setItem] = useState("")
const dispatch = useDispatch()
const navigate = useNavigate()
function handleSubmit(e) {
e.preventDefault()
let newItem = {description: item, completed: false, list_id: list.id}
dispatch(createListItem(newItem, list.id))
setItem("")
navigate(`/lists/${list.id}`)
}
return (
<div>
<br></br>
<form onSubmit={handleSubmit}>
<label>Add to your list: </label>
<input value={item} onChange={(e) => setItem(e.target.value)} />
</form>
</div>
)
}
I have been stuck on this for quite some time now and not sure where to go from here or where I am going wrong. Any help is appreciated!!

Sometimes when you navigate to "/lists/:id" you send route state, sometimes you don't. It's undefined when you navigate to "/lists/:id" when adding new list items. This navigation to the route you are already on for editing a list is unnecessary.
Since you are using Redux I don't think there's any need to send a list item in route state at all. Use the id route parameter and your lists redux state to derive the specific list you want to view/edit.
Example
Given: <Route path="/lists/:id" element={<ListContainer />} />
Lists
function Lists() {
const dispatch = useDispatch();
const lists = useSelector((state) => state.lists);
useEffect(() => {
if (!lists.length) {
dispatch(fetchLists());
}
}, [dispatch, lists]);
return (
<div>
{lists.map((list) => (
<Link key={list.id} to={`/lists/${list.id}`}>
<h2>{list.title}</h2>
</Link>
))}
</div>
);
}
ListContainer
import { useParams } from 'react-router-dom';
function ListContainer() {
const { id } = useParams();
const lists = useSelector((state) => state.lists);
const list = lists.find((list) => list.id === id);
return (
<div>
<List list={list} />
<ListItemForm list={list} />
</div>
);
}
ListItemForm
function ListItemForm({ list }) {
const [item, setItem] = useState("");
const dispatch = useDispatch();
function handleSubmit(e) {
e.preventDefault();
dispatch(actions.createListItem(item, list.id));
setItem("");
}
return (
<div>
<br></br>
<form onSubmit={handleSubmit}>
<label>Add to your list: </label>
<input value={item} onChange={(e) => setItem(e.target.value)} />
</form>
</div>
);
}

Related

react-hook-form: how to remove the item (which is mentioned in defautvalue) from submit data if its not mounted. Tried unregister its not working

I have simple form with firstName and lastName. I have added some defaultValues to them in useForm. I havent mounted the lastName using if condition.
Now when I try to submit, I am expecting it to show only the mounted components values i.e firstName. But it shows both firstName and lastName. I have created a button to try to unregister("lastName") but even after unregister and not being mounted it shows in the submit data.
If I dont provide defaultValues then it works well, like if not mounted initially it will not be shown in the submit data.
Below is the code
import React, { useState } from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";
import "./styles.css";
const App = () => {
const { register, handleSubmit, unregister, watch } = useForm({
defaultValues: {
firstName: "test",
lastName: "test2"
}
});
const onSubmit = (data) => {
console.log("ON SUBMIT");
console.log(JSON.stringify(data, null, 4));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>Last Name</label>
<input {...register("firstName")} />
{false && (
<>
<label>Last Name</label>
<input {...register("lastName")} />
</>
)}
<button
type="button"
onClick={() => {
console.log("unregistering lastName")
unregister("lastName");
}}
>
unregister lastName
</button>
<input type="submit" />
</form>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
And the codesandbox
For the first, you can't use unregister("lastName"); before register("lastName"); it's useless. Second, when you use defaultValues, method unregister does not remove them but simply set values to default. You just can filter submit data by conditions without unregister. Demo

TypeError: undefined is not an object (evaluating 'nod.images.fluid') --

I am new in Gatsby and trying to place a image carousel on a modal. I have an array of images on Contentful and cannot map the images. I can do it outside of the modal but inside I can't reach to the array .
I tried to use different modals, different carousels but this is the best result that I can find.
These are the codes for carousel:
const CarouselUI = ({ position, handleClick, children }) => (
<Container>
{children}
<Arrow onClick={handleClick} data-position={position - 1}>{'<'}</Arrow>
<Arrow right onClick={handleClick} data-position={position + 1}>{'>'}</Arrow>
</Container>
);
const Carousel = makeCarousel(CarouselUI)
These are the codes for component
const Product = ({ node }) => {
const [showModal, setShow] = useState(false);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
return (
<div>
<figure>
<Link href="/">
<Image fluid={node.image.fluid} alt={node.title} />
</Link>
<figcaption onClick={handleShow}>
<h4>Quick View</h4>
</figcaption>
<p>{node.title}</p>
<p>{node.price}</p>
<Modal className={product1Style.Modal} show={showModal} onHide={handleClose}>
<section className={product1Style.modalheader}>
<h1>{node.title}</h1>
</section>
<section className={product1Style.modalcontent}>
<Modal.Body className={product1Style.row + ' ' + product1Style.center} closeButton>
<div className={product1Style.col + ' ' + product1Style.colspan3}>
<h4>{node.price}</h4>
<div>
<button>Purchase</button>
</div>
</div>
<div className={product1Style.col + ' ' + product1Style.colspan4}>
<Carousel>
{node.images.map((nod) => {
return (
<Slide>
<Image fluid={nod.images.fluid} />
</Slide>
)
})}
</Carousel>
</div>
</Modal.Body>
</section>
</Modal>
</figure>
</div>
)
}
And this is my class:
class Product1 extends React.Component {
render() {
const ProductNecklace = this.props.data.productNecklace.edges
return (
<Layout>
<section className={product1Style.lowerBody}>
<section className={product1Style.product1Necklace}>
<h3>Product1 Necklace</h3>
<div className={product1Style.productnecklaceimage}>
{ProductNecklace.map(({ node }, i) => (
<Product node={node} key={node.id} />
))}
</div>
</section>
</section>
</Layout>
)
}
}
Your nod is the image itself, it's the iterable object from node.images, you can name it however you want. Without knowing the data structure, it's impossible to figure out how the map should be, but I guess that should look like:
<Carousel>
{node.images.map(image => {
console.log(image)
return (
<Slide>
<Image fluid={image.childImageSharp.fluid} />
</Slide>
)
})}
</Carousel>
If you provide how's the GraphQL query, I will be able to infer how are the object and the arrays. Assuming that your node.images holds an array of images, inside each image you will need to access first to childImageSharp property before reaching fluid. As I said, your GraphQL query will be easier to debug. The console.log above the return statement will help you to know the nested properties.

How to register 'react-bootstrap-typeahead' component using React 'useForm' hook?

I'm building a simple html form using "react-hook-form" library: https://react-hook-form.com/
I've incorporated "react-bootstrap-typeahead" into the html form but haven't been able to register this component with 'useForm' hook. Hence, "react-bootstrap-typeahead" input data is ignored during onSubmit.
"react-bootstrap-typeahead" doesn't provide a "name" prop which makes it difficult to register the component.
I've read the 'useForm' documentation on the different options for registering this type of components but still don't understand how to achieve this: https://react-hook-form.com/get-started#Registerfields
Does anybody have faced such challenge before?
It would be great to see a working example to get a better idea on how to implement "react-bootstrap-typeahead" + "react-hook-form" in my application. Thanks!
Here's my sample code:
import useForm from 'react-hook-form';
import { Typeahead } from 'react-bootstrap-typeahead';
import 'react-bootstrap-typeahead/css/Typeahead.css';
const myForm = (props) => {
const { register, handleSubmit, errors } = useForm();
const onSubmit = data => {
// api post request with form data
})
};
const mydata = [ "one", "two", "three" ];
return (
<>
<form onSubmit={handleSubmit(onSubmit)} >
<div className="form-group">
{/* Here I'm registering text input using ref: */}
<input type="text" className="form-control" name="name" ref={register({ required: true })} />
</div>
<div className="form-group mb-0">
{/* How can I register the below component with useForm? */}
<Typeahead
id="multiple-typeahead"
clearButton
multiple
options={mydata}
/>
</div>
<button type="submit">Save</button>
</form>
</>
);
}
This is how i was able to register the component:
import useForm from 'react-hook-form';
import { useForm, Controller } from "react-hook-form";
import 'react-bootstrap-typeahead/css/Typeahead.css';
const myForm = (props) => {
const { register, handleSubmit, errors, control } = useForm();
const onSubmit = data => {
// api post request with form data
})
};
const mydata = [ "one", "two", "three" ];
return (
<>
<form onSubmit={handleSubmit(onSubmit)} >
<div className="form-group">
<input type="text" className="form-control" name="name" ref={register({ required: true })} />
</div>
<div className="form-group mb-0">
<Controller
as={Typeahead}
control={control}
name="typeahead_component"
rules={{ required: true }}
id="multiple-typeahead"
clearButton
multiple
options={mydata}
defaultValue=""
/>
</div>
<button type="submit">Save</button>
</form>
</>
);
}

Unable to access mutator functions in Wizard form page while using react-final-form

I am trying to create a Wizard form using react-final-form by referring to this code https://codesandbox.io/s/km2n35kq3v. For my use case I need some mutator functions to be used inside my form fields. This example illustrates how to do that - https://codesandbox.io/s/kx8qv67nk5?from-embed.
I am not sure how to access mutator functions in my form steps when I am using a wizard form instead of a single page form.
I tried to combine both the examples by modifying the <Form> component rendered by Wizard.js to pass in the mutators. However I cannot access these mutators in the Wizard form pages.
In Wizard.js
return (
<Form
mutators={{
// potentially other mutators could be merged here
...arrayMutators,
}}
render={({
handleSubmit,
submitting,
values,
pristine,
invalid,
form: {
mutators: {push, pop, remove},
},
}) => {
return (
<form onSubmit={handleSubmit}>
Another file index.js
<Wizard
initialValues={{ employed: true, stooge: "larry" }}
onSubmit={onSubmit}
>
<Wizard.Page>
<FieldArray name="customers">
{({ fields }) =>
fields.map((name, index) => (
<div key={name}>
<label>Cust. #{index + 1}</label>
<Field
name={`${name}.firstName`}
component="input"
placeholder="First Name"
/>
<span
onClick={() => fields.remove(index)}
style={{ cursor: "pointer" }}
>
❌
</span>
</div>
))
}
</FieldArray>
</Wizard.Page>
</Wizard>
It errors out - remove is undefined in index.js
Look at this working example: https://codesandbox.io/s/znzlqvzvnx
changes I have made:
Wizard.js
static Page = ({ children, mutators }) => {
if(typeof children === 'function'){
return children(mutators);
}
return children;
};
...
<form onSubmit={handleSubmit}>
{
// activePage
<activePage.type {...activePage.props} mutators={mutators} />
}
...
index.js (only first <Wizard.page>)
<Wizard.Page>
{
({ upper }) => (
<React.Fragment>
<div>
<label>First Name</label>
<Field
name="firstName"
component="input"
...
</div>
</React.Fragment>
)
}
</Wizard.Page>

How to get the url to insert as a data with Redux Form?

I was able to get the url in the attachment field but for the redux form it was empty, how is possible to pass the value of the url to the redux form? Below is the code and the screenshot:
<div className="FileUpload">
<Dropzone
onDrop={this.onImageDrop.bind(this)}
multiple={false}
accept="image/*">
<div>Drop an image or click to select a file to upload.</div>
</Dropzone>
</div>
<div className="form-group">
<label htmlFor="attachment">Attachment:</label><br />
<input className="form-control" focus placeholder="attachment" type="text" name="attachment" ref="attachment" value={this.state.uploadedFileCloudinaryUrl} />
{this.state.uploadedFileCloudinaryUrl === '' ? null :
<div>
<p>{this.state.uploadedFile.name}</p>
<img src={this.state.uploadedFileCloudinaryUrl} alt="" />
</div>}
</div>
<div className="ui small image">
<img src={this.props.workrequest.attachment} alt="" />
</div>
the url in the attachemnt field
The first one is using the React Dropzone to get the url but for the Redux Form it was empty. May I know how to do that to get the url inserts at Redux Form here? Thank you
import React from 'React';
import { connect } from 'react-redux';
import { change, reduxForm } from 'redux-form';
import Dropzone from 'react-dropzone';
class UploadForm extends React.Component {
onDrop = (accepted) => {
if (!accepted.length) return;
// start uploading
this.setState({ isUploading: true });
const formData = new FormData();
formData.append('file', accepted[0]);
axios.post('upload', formData).then(
(res) => {
this.props.dispatch(change('uploadForm', 'url', res.url));
this.setState({ isUploading: false });
},
() => {
this.setState({ isUploading: false });
}
);
};
render() {
return (
<form>
<Dropzone onDrop={this.onDrop} />
</form>
);
}
}
export default connect()(
reduxForm({
form: 'uploadForm',
initialValues: {
url: ''
}
})(UploadForm)
);
Please use this.
Actually, you must use Field component from Redux-form.
Other wise, you can change form values using dispatch and change action creator.
import { Field, change } from 'redux-form';
this.props.dispatch(change('uploadForm', 'url', this.state.url));

Resources