React Child Component Causing Parent Re-Render on Ajax Call - ruby-on-rails

I am using React with React Router on top of Rails to handle the front end of an app that is supposed to return info about whatever gem the user searches for, however, once I hit submit, the child component, who manages it's own state, causes a re-render for the parent component.
EXPECTED RESULT: SavedGems.jsx re-renders
ACTUAL RESULT: Search.jsx re-renders
Here is my code:
StaticPage.jsx
export default class StaticPage extends React.Component {
render() {
return (
<BrowserRouter>
<div style={{display: 'flex', flexDirection: 'row'}}>
<Route exact path='/' render={() => <Search />}/>
<Route path='/favorites' render={() => <Favorites/>} />
</div>
</BrowserRouter>
);
}
}
Search.jsx
export default class Search extends React.Component {
render() {
return (
<div style = {{display: 'flex', flexDirection: 'row'}}>
<Sidebar/>
<div style = {{display: 'flex', flexDirection: 'column'}}>
<Header name = "Search Gems"/>
<Form/>
<SavedGems/>
</div>
</div>
)
}
}
SavedGems.jsx
export default class SavedGems extends React.Component {
constructor() {
super();
this.state = {saved_gems : []};
console.log(this.saved_gems);
}
componentDidMount() {
$.getJSON('/api/v1/saved_gems.json', (response) => { this.setState({ saved_gems: response }) });
}
render() {
var saved_gems= this.state.saved_gems.map((saved_gem) => {
return (
<div key={saved_gem.id}>
<h3>{saved_gem.name}</h3>
<h3>{saved_gem.info}</h3>
<h3>{saved_gem.dependencies}</h3>
</div>
)
});
return (
<div>
{saved_gems}
</div>
)
}
}
_form.jsx
export default class Form extends React.Component {
constructor() {
super();
this._handleClick = this._handleClick.bind(this);
this.state = {formBorderColor : "#5F5F5F"};
}
render() {
return (
<div>
<form>
<label>
<input ref='name'
type="text"
placeholder='Search'
style= {{fontFamily: 'Lato-Regular',
fontSize: 18,
height:89,
width: 780,
paddingLeft: 20,
backgroundColor: 'white',
border: '1px solid',
borderColor: this.state.formBorderColor,
borderRadius: 100}}/>
</label>
<input type="image" src='/assets/magnifying-glass.png'
style={{marginLeft: -70}}
onClick={this._handleClick}/>
</form>
</div>
)
}
_handleClick(event) {
const name = this.refs.name.value;
const info = '';
const dependencies = '';
$.ajax({
url: '/api/v1/saved_gems',
type: 'POST',
data: { saved_gem: { name, info, dependencies } },
success: (saved_gem) => {
this.props.handleSubmit(saved_gem);
this.refs.name.value = '';
this.refs.info.value = '';
this.refs.dependencies.value = '';
},
error: (xhr) => {
this.setState = ({formBorderColor : 'red'}).bind(this);
alert("Sorry! That is not a valid gem");
}
})
}
}

Related

React: Trying to set selection to drop down option

I setup a component that is basically a drop down and I am trying to figure out how to set it to where when I submit the form....its set on that one option. When I submit it now, it sends all the options to the backend instead of just the one I selected.
Here is my Category component
import React, { Component } from 'react'
class Categories extends Component{
handleCatChange = (event) => {
this.setState({category: event.target.value}) <------this should set the state to whatever is selected
}
render(){
let categories = this.props.category
let value = this.props.value
let optionItems = categories.map((cat,index) => {
return <option key={index} value={value}>{cat.category}</option>
})
return (
<div>
<select onchange={this.handleCatChange} value={this.props.category}>
{this.props.category ? optionItems : <p>Loading....</p>}
</select>
</div>
)
}
}
export default Categories
And here is RecipeInput Component with form
import React, { Component } from 'react'
import Categories from './Categories.js'
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
name:'',
ingredients: '',
chef_name: '',
origin: ''
}
}
componentDidMount(){
let initialCats = [];
const BASE_URL = `http://localhost:10524`
const CATEGORIES_URL =`${BASE_URL}/categories`
fetch(CATEGORIES_URL)
.then(resp => resp.json())
.then(data => {
initialCats = data.map((category) => {
return category
})
this.setState({
category: initialCats
})
});
}
handleSubmit = (event) =>{
event.preventDefault();
this.props.postRecipes(this.state)
this.setState({
name:'',
ingredients: '',
chef_name: '',
origin: ''
})
}
render(){
return(
<div>
<form onSubmit={this.handleSubmit}>
<Categories category={this.state.category} value={this.state.category}/>
<div>
<label for='name'>Recipe Name:</label>
<input type='text' value={this.state.name} onChange={this.handleNameChange} />
</div>
<div>
<label for='name'>Country Origin:</label>
<input type='text' value={this.state.origin} onChange={this.handleOriginChange} />
</div>
<div>
<label for='name'>Chef Name:</label>
<input type='text' value={this.state.chef_name} onChange={this.handleChefChange} />
</div>
<div>
<label for='name'>Ingredients:</label>
<textarea value={this.state.ingredients} onChange={this.handleIngChange} />
</div>
<input value='submit' type='submit'/>
</form>
</div>
)
}
}
export default RecipeInput
And here is the error that is produced on submission(Its Rails btw)
I tired a few ways but haven't quite wrapped my head around using a component as a dropdown. What do I need to do?
Here is my backend code that creates the record on the api
def create
recipe = Recipe.create(recipe_params)
if recipe.save
render json: recipe
else
render json: { error: "Couldn't save" }
end
end
private
def recipe_params
params.permit(:category_id,:name,:ingredients,:chef_name,:origin,category_attribute:[:category])
end
Also my postRecipe function
export const postRecipes = (recipe)=>{
const BASE_URL = `http://localhost:10524`
const RECIPES_URL =`${BASE_URL}/recipes`
const config = {
method: "POST",
body:JSON.stringify(recipe),
headers: {
"Accept": "application/json",
"Content-type": "application/json"
}
}
//category field
return(dispatch)=>{
fetch(RECIPES_URL,config)
.then(response => response.json())
.then(resp => {
dispatch({
type: 'Add_Recipe',
payload:{
// category:resp.category,
name: resp.name,
ingredients: resp.ingredients,
chef_name: resp.chef_name,
origin: resp.origin,
categoryId: resp.categoryId
}
})
})
//.then(response => <Recipe />)
.catch((error) => console.log.error(error))
}
}
Code Edit due to change in question:
Access selectedValue while sending to the server
class Categories extends Component {
render() {
...
let optionItems = categories.map((cat, index) => {
return (
<option key={index} value={index}>
{cat.category}
</option>
);
});
...
}
}
class RecipeInput extends Component{
constructor(props){
super(props)
this.state = {
category: [],
name:'',
ingredients: '',
chef_name: '',
origin: ''
selectedValue: {}
}
}
handleSubmit(id){
this.setState({
selectedValue: this.state.category[id]
)}
}
...
}
You're passing onChange and value from Input Component but you're not using them in Categories Component.
Add onChange and value property to tag.
here is reference
import "./styles.css";
import React, { Component } from "react";
class Categories extends Component {
render() {
let categories = this.props.category;
let onChange = this.props.onChange;
let optionItems = categories.map((cat, index) => {
return (
<option key={index} value={cat.category}>
{cat.category}
</option>
);
});
return (
<div>
<select onChange={(e) => onChange(e.target.value)}>
{this.props.category.length ? optionItems : null}
</select>
</div>
);
}
}
export default function App() {
const onChange = (value) => {
console.log(value);
};
return (
<Categories
onChange={onChange}
category={[{ category: "1st" }, { category: "2nd" }]}
/>
);
}
I've updated the code.
If you need to try it online you can refer my Sandbox
https://codesandbox.io/s/stackoverflow-qno-65730813-j32ce

Allow Downshift useCombobox to select items not in the list

I'm using useCombobox from Downshift as a use-hook-form component and everything works fine except that I can't get the value when a user types in a value not in the list that is passed into useComboBox.
onSelectedItemChange is never fired unless the value is in the inputItems. This seems like it should be easy but I can't find an answer from the docs.
import React, { memo, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useCombobox } from 'downshift';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faChevronDown } from '#fortawesome/free-solid-svg-icons';
const comboboxStyles = { display: 'inline-block', marginLeft: '5px' };
let Item = ({ isHighlighted, getItemProps, item, index }) => {
return (
<li
className="auto-complete-list-item"
style={isHighlighted ? { backgroundColor: '#bde4ff' } : {}}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
);
};
Item = memo(Item);
const Autocomplete = ({ items, onChange, isSubmitting }) => {
const [inputItems, setInputItems] = useState(items);
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps,
inputValue,
reset
} = useCombobox({
items: inputItems,
onSelectedItemChange: ({ inputValue }) => onChange(inputValue),
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(item =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
);
}
});
useEffect(() => {
if (inputValue.length > 0 && isSubmitting) reset();
}, [inputValue, isSubmitting, reset]);
return (
<div className="input-field">
<div style={comboboxStyles} {...getComboboxProps()}>
<input name="autocomplete" {...getInputProps()} />
<button
type="button"
{...getToggleButtonProps()}
aria-label="toggle menu"
>
<FontAwesomeIcon icon={faChevronDown} />
</button>
</div>
<ul {...getMenuProps()} className="auto-complete-list">
{isOpen &&
inputItems.map((item, index) => (
<Item
key={item}
isHighlighted={highlightedIndex === index}
getItemProps={getItemProps}
item={item}
index={index}
/>
))}
</ul>
</div>
);
};
Autocomplete.propTypes = {
list: PropTypes.array
};
export default Autocomplete;
You need to take control of item selection:
const {
isOpen,
selectItem,
getToggleButtonProps,
...
}
Then call selectItem in an onClick handler:
<Item
key={item}
onClick={() => selectItem(item)}
isHighlighted={highlightedIndex === index}
...
/>

Using useComboBox from DownShift with react-hook-form

I'm trying to use useComboBox from DownShift with react-hook-form and the value of the input is always undefined. I started with this: https://codesandbox.io/s/react-hook-form-controller-079xx?file=/src/DonwShift.js
And replaced the DownShift.js component with this: https://codesandbox.io/s/usecombobox-usage-1fs67?file=/src/index.js:168-438
Everything works except when I submit the value is undefined.What am I missing to set the value?
<form className="card" onSubmit={handleSubmit(handleShare)}>
<div className="body">
<Controller
as={Autocomplete}
control={control}
name="recipient"
items={userList}
/>
<button
className="secondaryActionBtn inputBtn"
type="submit"
enabled={String(formState.dirty)}
>
<FontAwesomeIcon icon={faPlus} />
</button>
{errors.lastname && 'Feed Name is required.'}
</div>
<footer></footer>
</form>
import React, { memo, useState } from 'react';
import PropTypes from 'prop-types';
import { useCombobox } from 'downshift';
const menuStyles = {
maxHeight: '180px',
overflowY: 'auto',
width: '135px',
margin: 0,
borderTop: 0,
background: 'white',
position: 'absolute',
zIndex: 1000,
listStyle: 'none',
padding: 0,
left: '135px'
};
const comboboxStyles = { display: 'inline-block', marginLeft: '5px' };
function Item({ isHighlighted, getItemProps, item, index }) {
return (
<li
style={isHighlighted ? { backgroundColor: '#bde4ff' } : {}}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
);
}
Item = memo(Item);
const Autocomplete = ({ items }) => {
const [inputItems, setInputItems] = useState(items);
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps
} = useCombobox({
items: inputItems,
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(item =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
);
}
});
return (
<div>
<label htmlFor="recipient" {...getLabelProps()}>
Choose an element:
</label>
<div style={comboboxStyles} {...getComboboxProps()}>
<input name="recipient" {...getInputProps()} id="recipient" />
<button {...getToggleButtonProps()} aria-label="toggle menu">
↓
</button>
</div>
<ul {...getMenuProps()} style={menuStyles}>
{isOpen &&
inputItems.map((item, index) => (
<Item
key={item}
isHighlighted={highlightedIndex === index}
getItemProps={getItemProps}
item={item}
index={index}
/>
))}
</ul>
</div>
);
};
Autocomplete.propTypes = {
list: PropTypes.array
};
export default Autocomplete;
For others who get stuck on this here's how I solved it. The Controller in react-hook-form injects an onChange into the component as a prop. So i set the onSelectedItemChange prop in useCombobox hook to pass its value into onChange. Like this:
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getComboboxProps,
highlightedIndex,
getItemProps
} = useCombobox({
items: inputItems,
onSelectedItemChange: ({ inputValue }) => onChange(inputValue),
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(item =>
item.toLowerCase().includes(inputValue.toLowerCase())
)
);
}
});

Cannot read property id of undefined, error found in the map function (react-rails)

I'm trying to make my submit button add on to the list of items, my seed data in rails gets rendered fine but when I try to add a new item when clicking submit, I get this error Cannot read property id of undefined which is found at the map function. The post request works fine, and only after I refresh the page, the item I add gets rendered on the page. Any help would be appreciated!
class TodoList extends React.Component {
render() {
const {todos} = this.props
var todoItems = todos.map(title => <TodoItem key={title.id} title={title}/>) //here
return (
<ListGroup className="my-2">
<h2 className="text-center">Items</h2>
{todoItems}
</ListGroup>
)
}
}
export default TodoList
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
todos: [],
stuff: ''
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
this.addNewTodo = this.addNewTodo.bind(this)
}
handleChange = event => {
this.setState({
stuff:event.target.value
})
}
handleSubmit = event => {
event.preventDefault()
let body = JSON.stringify({todo: {item: this.state.stuff} })
fetch('http://localhost:3000/api/v1/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: body,
})
.then(response => {response.json()})
.then(todo => {this.addNewTodo(todo)})
}
addNewTodo(todo){
this.setState({
todos: this.state.todos.concat(todo)
})
}
render() {
return (
<div className="container">
<div className="row">
<div className="col-10 mx-auto mt-4">
<h1 className="text-center">Todo List</h1>
<TodoInput
stuff={this.state.stuff}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
/>
<TodoList
todos={this.state.todos}
/>
</div>
</div>
</div>
)
}
}
when your component is renders first "todos" won't have any value, it will be undefined. try below code
class TodoList extends React.Component {
render() {
const { todos } = this.props;
if (todos) {
return (
<ListGroup className="my-2">
<h2 className="text-center">Items</h2>
{todos.map(title => (
<TodoItem key={title.id} title={title} />
))}
</ListGroup>
);
}
return ""; } }
export default TodoList;

React Native, Webview iOS not rendering

I'm building an app with React-Native.
I'm trying to render a URL with a Webview, even some basic HTML or a site like m.facebook.com will not render in iOS. Tried different solution like other Webviews but none give results.
When in Android i don't have any problems and the page(s) will render just fine. Am i missing some key information.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions } from 'react-native-navigation-redux-helpers';
import { Container, Header, Title, Content, Button, Icon, Footer } from 'native-base';
import { Platform, WebView } from 'react-native';
import FooterTabs from '../../components/footerTabs/FooterTabs';
import { openDrawer } from '../../actions/drawer';
import { setWebsiteUrl } from '../../actions/website';
import styles from './styles';
const {
pushRoute,
popRoute,
} = actions;
class Website extends Component {
constructor(props, context) {
super(props, context);
this.state = {};
this.openFrame = this.openFrame.bind(this);
}
popRoute() {
this.props.popRoute(this.props.navigation.key);
}
pushRoute(route, index) {
this.props.pushRoute({ key: route, index: 1 }, this.props.navigation.key);
}
openFrame(url, name) {
this.props.setWebsiteUrl(url, name);
this.pushRoute('website', 2);
}
render() {
const { props } = this;
const { website, totalQuantity } = props;
const { frameUrl, frameName } = website;
return (
<Container style={styles.container} theme={deenTheme}>
<Header toolbarDefaultBg="#FFF" toolbarTextColor="FBFAFA">
<Button transparent onPress={this.props.openDrawer}>
<Icon name="ios-menu" />
</Button>
<Title style={styles.headerText}>{frameName}</Title>
<Button transparent onPress={() => this.openFrame('/cart', 'Winkelwagen')}>
<Icon style={{ fontSize: 25 }} name="md-basket" />
</Button>
<Button transparent>
<Icon style={{ fontSize: 25 }} name="md-search" />
</Button>
</Header>
<Content>
<WebView
source={{ uri: frameUrl }}
startInLoadingState
javaScriptEnabledAndroid
javaScriptEnabled
domStorageEnabled
scrollEnabled
style={{ flex: 1, width: 320 }}
/>
</Content>
<Footer theme={deenTheme}>
<FooterTabs />
</Footer>
</Container>
);
}
}
Website.propTypes = {
totalQuantity: React.PropTypes.number,
openDrawer: React.PropTypes.func,
setWebsiteUrl: React.PropTypes.func,
popRoute: React.PropTypes.func,
pushRoute: React.PropTypes.func,
navigation: React.PropTypes.shape({
key: React.PropTypes.string,
}),
};
function bindAction(dispatch) {
return {
openDrawer: () => dispatch(openDrawer()),
popRoute: key => dispatch(popRoute(key)),
pushRoute: (route, key) => dispatch(pushRoute(route, key)),
setWebsiteUrl: (url, name) => dispatch(setWebsiteUrl(url, name)),
};
}
const mapStateToProps = state => ({
navigation: state.cardNavigation,
website: state.website,
totalQuantity: state.cart.totalQuantity,
});
export default connect(mapStateToProps, bindAction)(Website);
UPDATE!
in iOS i need to configure style width & height else it won't work.

Resources