I'm working on my first React/Redux project and I have a little question. I've read the documentation and watched the tutorials available at https://egghead.io/lessons/javascript-redux-generating-containers-with-connect-from-react-redux-visibletodolist.
But I still have one question. It's about a login page.
So I have a presentational component named LoginForm :
components/LoginForm.js
import { Component, PropTypes } from 'react'
class LoginForm extends Component {
render () {
return (
<div>
<form action="#" onSubmitLogin={(e) => this.handleSubmit(e)}>
<input type="text" ref={node => { this.login = node }} />
<input type="password" ref={node => { this.password = node }} />
<input type="submit" value="Login" />
</form>
</div>
)
}
handleSubmit(e) {
e.preventDefault();
this.props.onSubmitLogin(this.login.value, this.password.value);
}
}
LoginForm.propTypes = {
onSubmitLogin: PropTypes.func.isRequired
};
export default LoginForm;
And a container component named Login which pass data to my component. Using react-redux-router, I call this container (and not the presentationnal component) :
containers/Login.js
import { connect } from 'react-redux'
import { login } from '../actions/creators/userActionCreators'
import LoginForm from '../components/LoginForm'
const mapDispatchToProps = (dispatch) => {
return {
onSubmitLogin: (id, pass) => dispatch(login(id, pass))
}
};
export default connect(null, mapDispatchToProps)(LoginForm);
As you can see, I'm using the connect method provide by redux to create my container.
My question is the following one :
If I want my Login container to use multiple views (for example : LoginForm and errorList to display errors), I need to do it by hand (without connect because connect take only one argument). Something like :
class Login extends Component {
render() {
return (
<div>
<errorList />
<LoginForm onSubmitLogin={ (id, pass) => dispatch(login(id, pass)) } />
</div>
)
}
}
Is it a bad practice ? Is it better to create another presentational component (LoginPage) which use both errorList and LoginForm and create a container (Login) which connect to LoginPage ?
EDIT: If I create a third presentational component (LoginPage), I'll have to pass data twice. Like this : Container -> LoginPage -> LoginForm & ErrorList.
Even with context, it don't seems to be the way to go.
I think that what you have in your second example is very close. You can create just one container component that's connected and render multiple presentational components.
In your first example, there actually isn't a separate container component:
import { connect } from 'react-redux'
import { login } from '../actions/creators/userActionCreators'
import LoginForm from '../components/LoginForm'
const mapDispatchToProps = (dispatch) => {
return {
onSubmitLogin: (id, pass) => dispatch(login(id, pass))
}
};
// `LoginForm` is being passed, so it would be the "container"
// component in this scenario
export default connect(null, mapDispatchToProps)(LoginForm);
Even though it's in a separate module, what you're doing here is connecting your LoginForm directly.
Instead, what you can do is something like this:
containers/Login.js
import { connect } from 'react-redux'
import { login } from '../actions/creators/userActionCreators'
import LoginForm from '../components/LoginForm'
import ErrorList from '../components/ErrorList'
class Login extends Component {
render() {
const { onSubmitLogin, errors } = this.props;
return (
<div>
<ErrorList errors={errors} />
<LoginForm onSubmitLogin={onSubmitLogin} />
</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onSubmitLogin: (id, pass) => dispatch(login(id, pass))
}
};
const mapStateToProps = (state) => {
return {
errors: state.errors
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Login);
Note that the Login component is now being passed to connect, making it the "container" component and then both the errorList and LoginForm can be presentational. All of their data can be passed via props by the Login container.
I truly believe that you need to build all your components as Presentational Components. At the moment you need a Container Component, you might use {connect} over one of the existing Presentational and convert into a Container one.
But, that is only my view with short experience in React so far.
Related
I am having a question about how to implement a callback function. In my case, I have a React app with this structure: App > Child > Button components
The problem is I do not know how to write a callback function from Button to Child
I would like to update a value in Child (e.g: inputFromButton) after clicking the button in Button Component. The handleClick() is triggered and a value will be sent to the Child component.
Could someone help me to do this?
Here is my code:https://codesandbox.io/s/nifty-stonebraker-0950w8
The App component
import React from 'react';
import Child from './Child';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: 'Data from App'
}
}
handleCallback = (childData) => {
this.setState({ data: childData })
}
render() {
const { data } = this.state;
return (
<div>
<Child dataFromApp={data} />
</div>
)
}
}
export default App
The Child component
import React from 'react';
import { renderButton } from './Button';
class Child extends React.Component {
state = {
inputFromApp: "",
inputFromButton: ""
}
componentDidMount() {
this.setState({
inputFromApp: this.props.dataFromApp
})
}
render() {
const renderButtonItem = renderButton(this.props);
const inputFromApp = this.state.inputFromApp
const inputFromButton= this.state.inputFromButton
return (
<div>
<input value={inputFromApp}></input>
<br></br>
<input value={inputFromButton}></input>
<div>{renderButtonItem}</div>
</div>
)
}
}
export default Child
The Button component
import React from 'react';
export const renderButton = (props) => {
const handleClick = () => {
console.log('handleClick() props data from App: ' + props.dataFromApp)
}
return (
<button onClick={handleClick}>Click</button>
)
}
renderButton is a function component and, therefore, needs to be in PascalCase: RenderButton (although it would be better off as Button).
Move handleClick to the Child component.
Then in Button the call to handleClick should be props.handleClick since handleClick will now be a property of the props object passed into the component. We don't need to pass down the data as a prop to the button but can, instead just log the data prop passed into Child.
handleClick = () => {
console.log(`handleClick(): ${props.dataFromApp}`);
}
In Child, instead of calling renderButton, import Button, and then use that in the render passing down the handler in the props. By doing this you're making the component as "dumb" as possible so it can be reused elsewhere in the application.
<Button handleClick={this.handleClick} />
I'm pretty sure I'm just 100% missing the point on this as I'm new to React and Javascript but.
When I call another component in my main what is exactly happening with props in these two pieces of code? What is table={table} shouldn't we be calling it props? and then when I pass props to my other component why is it being stored as a const with the point of it being props doesn't it already have those values?
import RecordsGetter from './RecordsGetter'
function MainController() {
const base = useBase();
console.log("The name of the base is: ", base);
const tables = base.tables;
console.log("The name of the tables are: ", tables);
return (
<div>
{tables.map((table) => {
return (
<div>
<br />
<div>{table.name}</div>
<div>{table.id}</div>
<div>{table.description}</div>
<RecordsGetter table={table}/>
</div>
);
})}
</div>
);
}
export default MainController;
import React from 'react';
import {useBase} from '#airtable/blocks/ui'
export default function RecordsGetter (props) {
const {
table
} = props;
const records = useRecords(table);
console.log('records', records);
return (<div></div>)
}
props enable you to pass variables from one to another component down the component tree.
<RecordsGetter table={table}/>
ur passing the recrodsgetter data to another component which then can be read by another component. props can be anything from integers over objects to arrays. Props are read-only. In a functional stateless component, the props are received in the function signature as arguments:
for example:
import React, { Component } from 'react';
class App extends Component {
render() {
const greeting = 'Welcome to React';
return (
<div>
<Greeting greeting={greeting} />
</div>
);
}
}
const Greeting = props => <h1>{props.greeting}</h1>;
export default App;
this is the same thing as
import React, { Component } from 'react';
class App extends Component {
render() {
const greeting = 'Welcome to React';
return (
<div>
<Greeting greeting={greeting} />
</div>
);
}
}
class Greeting extends Component {
render() {
return <h1>{this.props.greeting}</h1>;
}
}
export default App;
the result output would be the same in both cases. we are passing down the parameters of greeting to another component. Which in your case you are passing the table params
I have a Component like this:
class GlobalComponent extends React.Component{
select= (e) => {
this.props.select(); // must be implemented in another component and not parent
};
render(){
return(
<div>
{this.state.data}
<Button onClick={this.select}>Select</Button>
</div>
);
}
}
function mapStateToProps(state) {
return state.data
}
const mapDispatchToProps = (dispatch, props) => ({
select: () => dispatch(select()),
});
export default connect(mapStateToProps, mapDispatchToProps)(GlobalComponent );
I am importing this component in the root of my app - app.js like this:
<GlobalComponent />
The problem is this actionselect. I know I can pass it from global state or parent component with props (for the example):
<GlobalComponent select={this.select} />
or with mapDispatchToProps
, but the problem is that the component is defined only in one place, otherwise I need to import the GlobalComponent in almost all components.
What I want is to import the component once (somewhere in the root) and then somehow in a component that uses GlobalComponent define a function, which will NOT execute select, but implement select and it is only for the click event in the GlobalComponent:
export default class ComponentThatUsesGlobal extends React.Component {
executeWhenGlobalComponentBtnIsClicked = () => {
}
}
Is this possible and is it good approach or it is better just to import the component everywhere ?
I am using and Redux.
I'm having an issue where react-loadable is causing one of my input components to re-render and lose focus after a state update. I've done some digging and I can't find anyone else having this issue, so I think that I'm missing something here.
I am attempting to use react-loadable to dynamically include components into my app based on a theme that the user has selected. This is working fine.
./components/App
import React from 'react';
import Loadable from 'react-loadable';
/**
* Import Containers
*/
import AdminBar from '../../containers/AdminBar';
import AdminPanel from '../../components/AdminPanel';
import 'bootstrap/dist/css/bootstrap.css';
import './styles.css';
const App = ({ isAdmin, inEditMode, theme }) => {
const MainContent = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/MainContent'),
loading: () => (<div>Loading...</div>)
});
const Header = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/Header'),
loading: () => (<div>Loading...</div>)
});
return (
<div>
{
(isAdmin) ? <AdminBar
className='admin-bar'
inEditMode={inEditMode} /> : ''
}
<Header
themeSettings={theme.settings.Header} />
<div className='container-fluid'>
<div className='row'>
{
(isAdmin && inEditMode) ? <AdminPanel
className='admin-panel'
theme={theme} /> : ''
}
<MainContent
inEditMode={inEditMode} />
</div>
</div>
</div>
);
};
export default App;
./components/AdminPanel
import React from 'react';
import Loadable from 'react-loadable';
import './styles.css';
const AdminPanel = ({ theme }) => {
const ThemedSideBar = Loadable({
loader: () => import('../../themes/' + theme.name + '/components/SideBar'),
loading: () => null
});
return (
<div className='col-sm-3 col-md-2 sidebar'>
<ThemedSideBar
settings={theme.settings} />
</div>
);
};
export default AdminPanel;
This is what my <ThemedSideBar /> components looks like:
./themes/Default/components/SideBar
import React from 'react';
import ThemeSettingPanel from '../../../../components/ThemeSettingPanel';
import ThemeSetting from '../../../../containers/ThemeSetting';
import './styles.css';
const SideBar = ({ settings }) => {
return (
<ThemeSettingPanel
name='Header'>
<ThemeSetting
name='Background Color'
setting={settings.Header}
type='text'
parent='Header' />
<ThemeSetting
name='Height'
setting={settings.Header}
type='text'
parent='Header' />
</ThemeSettingPanel>
);
};
export default SideBar;
./components/ThemeSettingPanel
import React from 'react';
import { PanelGroup, Panel } from 'react-bootstrap';
const ThemeSettingPanel = ({ name, children }) => {
return (
<PanelGroup accordion id='sidebar-accordion-panelGroup'>
<Panel>
<Panel.Heading>
<Panel.Title toggle>{name}</Panel.Title>
</Panel.Heading>
<Panel.Body collapsible>
{children}
</Panel.Body>
</Panel>
</PanelGroup>
);
};
export default ThemeSettingPanel;
./containers/ThemeSetting
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { themeSettingChange } from '../App/actions';
import ThemeSetting from '../../components/ThemeSetting';
class ThemeSettingContainer extends Component {
constructor(props) {
super(props);
this.handleOnChange = this.handleOnChange.bind(this);
}
handleOnChange(name, parent, value) {
const payload = {
name: name,
parent,
value: value
};
this.props.themeSettingChange(payload);
}
render() {
return (
<ThemeSetting
name={this.props.name}
setting={this.props.setting}
parent={this.props.parent}
type={this.props.type}
handleOnChange={this.handleOnChange} />
);
}
}
//----Redux Mappings----//
const mapStateToProps = (state) => ({
});
const mapDispatchToProps = {
themeSettingChange: (value) => themeSettingChange(value)
};
export default connect(mapStateToProps, mapDispatchToProps)(ThemeSettingContainer);
./component/ThemeSetting
import React from 'react';
import TextField from '../common/TextField';
import './styles.css';
const ThemeSetting = ({ name, setting, type, parent, handleOnChange }) => {
return (
<div className='row theme-setting'>
<div className='col-xs-7'>
{name}
</div>
<div className='col-xs-5'>
{
generateField(type, setting, name, parent, handleOnChange)
}
</div>
</div>
);
};
function generateField(type, setting, name, parent, handleOnChange) {
const value = setting ? setting[name] : '';
switch (type) {
case 'text':
return <TextField
value={value}
name={name}
parent={parent}
handleOnChange={handleOnChange} />;
default:
break;
}
}
export default ThemeSetting;
./components/common/TextField
import React from 'react';
import { FormControl } from 'react-bootstrap';
const TextField = ({ value, name, parent, handleOnChange }) => {
return (
<FormControl
type='text'
value={value}
onChange={(e) => {
handleOnChange(name, parent, e.target.value);
}} />
);
};
export default TextField;
When a field inside of my Admin Panel is updated, a state change is triggered. It seems like this triggers react-loadable to re-render my <ThemedSideBar /> components which destroys my input and creates a new one with the updated value. Has anyone else had this issue? Is there a way to stop react-loadable from re-rendering?
EDIT: Here is the requested link to the repo.
EDIT: As per conversation in the comments, my apologies, I misread the question. Answer here is updated (original answer below updated answer)
Updated answer
From looking at the react-loadable docs, it appears that the Loadable HOC is intended to be called outside of a render method. In your case, you are loading ThemedSideBar in the render method of AdminPanel. I suspect that the change in your TextEdit's input, passed to update your Redux state, and then passed back through the chain of components was causing React to consider re-rendering AdminPanel. Because your call to Loadable was inside the render method (i.e. AdminPanel is a presentational component), react-loadable was presenting a brand new loaded component every time React hit that code path. Thus, React thinks it needs to destroy the prior component to appropriately bring the components up to date with the new props.
This works:
import React from 'react';
import Loadable from 'react-loadable';
import './styles.css';
const ThemedSideBar = Loadable({
loader: () => import('../../themes/Default/components/SideBar'),
loading: () => null
});
const AdminPanel = ({ theme }) => {
return (
<div className='col-sm-3 col-md-2 sidebar'>
<ThemedSideBar
settings={theme.settings} />
</div>
);
};
export default AdminPanel;
Original answer
It seems that your problem is likely related to the way you've built TextField and not react-loadable.
The FormControl is taking value={value} and the onChange handler as props. This means you've indicated it is a controlled (as opposed to uncontrolled) component.
If you want the field to take on an updated value when the user types input, you need to propagate the change caught by your onChange handler and make sure it gets fed back to the value in the value={value} prop.
Right now, it looks like value will always be equal to theme.settings.Height or the like (which is presumably null/empty).
An alternative would be to make that FormControl an uncontrolled component, but I'm guessing you don't want to do that.
I need to wrap functionality in a, lets say button. However when I call the HOC in the render method of another component I get nothing.
I have this HOC
import React,{Component,PropTypes} from 'react';
export let AddComment = (ComposedComponent) => class AC extends React.Component {
render() {
return (
<div class="something">
Something...
<ComposedComponent {...this.props}/>
</div>
);
}
}
and trying to do this
import {AddComment} from '../comments/add.jsx';
var Review = React.createClass({
render: function(){
return (
<div className="container">
{AddComment(<button>Add Comment</button>,this.props)}
</div>
});
module.exports = Review;
I want AddComment to open a Dialog and submit a comments form when I click the button. I need AddComment to be available other components throughtout the app.
Is the HOC pattern correct? How can I easily accomplish this?
Thanks
To summarize really quick: What are higher-order components?
Just a fancy name for a simple concept: Simply put: A component that takes in a component and returns you back a more enhanced version of
the component.
We are essentially enhancing a component.
Accepts a function that maps owner props to a new collection of props
that are passed to the base component.
We are basically passing the props down from that BaseComponent down
to the Wrapped Component so that we can have them available in that
child component below:
Use to compose multiple higher-order components into a single
higher-order component.
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { AddComment } from '../comments/add.jsx';
const mapProps = propFunction => Component => (props) => {
return React.createFactory(Component)(propFunction(props));
};
const compose = (propFunction, ComponentContainer) => (BaseComponent) => {
return propFunction(ComponentContainer(BaseComponent));
};
const Review = AddComment(({ handleReviewToggle }) => (
<div className="container">
<ReviewButton
primaryText="Add Comment"
_onClick={handleReviewToggle}
/>
</div>
));
export default Review;
// ================================================================== //
const EnhanceReview = compose(withProps, AddComment)(Review);
const withProps = mapProps(({ ...props }) => ({ ...props }));
The AddComment Container that will have the button and the dialog itself.
export function AddComment(ComposedComponent) {
class AC extends React.Component {
constructor() {
super();
this.state = {open: false};
}
handleReviewToggle = () => {
this.setState({ open: !this.state.open })
}
render() {
return (
<ComposedComponent
{...this.props}
{...this.state}
{...{
handleReviewToggle: this.handleReviewToggle,
}}
/>
);
}
}
export default AddComment;
// ==================================================================
The ReviewButton Button that will fire an event to change state true or false.
const ReviewButton = ({ _onClick, primaryText }) => {
return (
<Button
onClick={_onClick}
>
{primaryText || 'Default Text'}
</Button>
);
};
export default ReviewButton;
// ================================================================== //
However this was all done without using a library. There's one out called recompose here: https://github.com/acdlite/recompose. I highly suggest that you try it out without a library to get a good understanding of Higher Order Components.
You should be able to answer these questions below after playing with Higher Order components:
What is a Higher Order Component?
What are the disadvantages of using HOC? What are some use cases?
How will this improve performance? And how can I use this to optimize for performance?
When is the right time to use a HOC?