I'm starting to implement the smart/dumb component pattern where the "dumb" component knows nothing about it's environment and receives all of it's data through props. What do you do when the dumb component has to submit or change data? How can it communicate with the outside world and still be "dumb"?
Here's my basic example I'm using to figure this pattern out. If I were to add and onClick event to the MyTask component that updated a counter in the DB, what would handle that event?
// components/MyList.jsx
import React from 'react';
export default class MyList extends React.Component {
render() {
return(
<div>
<h3>{this.props.listName}</h3>
<ul>
{this.props.tasks.map((task) => (
<MyTask key={task.id} task={task} />
))}
</ul>
</div>
);
}
}
MyList.propTypes = {
listName: React.PropTypes.string.isRequired,
tasks: React.PropTypes.array.isRequired,
}
export class MyTask extends React.Component {
render() {
return (
<li>{this.props.task.text}</li>
);
}
}
MyTask.propTypes = {
task: React.PropTypes.object.isRequired,
}
and the app:
// app.jsx
import React from 'react';
import MyList from './components/MyList.jsx'
export class TaskApp extends React.Component {
getList() {
return('Today Stuff');
}
getTasks() {
return([
{id: 1, text: 'foo'},
{id: 2, text: 'diggity'},
{id: 3, text: 'boo'},
{id: 4, text: 'bop'}
]);
}
render() {
return (
<MyList listName={this.getList()} tasks={this.getTasks()} />
);
}
}
Generally speaking you can handle this by passing function references from your 'smart' components down to your 'dumb' components. The dumb component then isn't responsible for implementing any of the logic associated with the function, just telling the smart component 'I've been clicked'.
In this case inside of your TaskApp class in app.jsx you could have your click handler:
//app.jsx
...
handleClick() {
// Update the DB counter by 1
}
...
render () {}
Then pass handleClick through your components as a prop:
<MyList listName={this.getList()} tasks={this.getTasks()} handleClick={this.handleClick} />
<MyTask key={task.id} task={task} handleClick={this.props.handleClick} />
And execute it in the MyTask component when a list element is clicked:
<li onClick={this.props.handleClick}>{this.props.task.text}</li>
Keep in mind that if the handleClick() function is making use of 'this' at all, you'll need to .bind(this) on your function reference when you pass it down (or bind it in the constructor / use ES6 fat arrow functions).
EDIT: For examples of the other ways to bind 'this', you could in the constructor of your class assign the bound function to your this.handleClick reference, so:
export default class TaskApp extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
...
...
}
Which allows you to use this.handleClick the way you would normally expect.
Or you could use ES6 fat arrow functions, which preserve the context of 'this' when they are called:
<MyList
listName={this.getList()}
tasks={this.getTasks()}
handleClick={() => this.handleClick} />
Assuming that TaskApp is the smart component and MyList is the dumb component, it should be something like
Smart Component
// app.jsx
import React from 'react';
import MyList from './components/MyList.jsx'
export class TaskApp extends React.Component {
getList() {
return('Today Stuff');
}
getTasks() {
return([
{id: 1, text: 'foo'},
{id: 2, text: 'diggity'},
{id: 3, text: 'boo'},
{id: 4, text: 'bop'}
]);
}
handleClick(task){
// update the db here
}
render() {
return (
<MyList listName={this.getList()} tasks={this.getTasks()}
onClick={this.handleClick}/>
);
}
}
Dumb Component
// components/MyList.jsx
import React from 'react';
export default class MyList extends React.Component {
render() {
return(
<div>
<h3>{this.props.listName}</h3>
<ul>
{this.props.tasks.map((task) => (
<MyTask onClick={() => this.props.onClick(task)}
key={task.id} task={task} />
))}
</ul>
</div>
);
}
}
MyList.propTypes = {
listName: React.PropTypes.string.isRequired,
tasks: React.PropTypes.array.isRequired,
}
export class MyTask extends React.Component {
render() {
return (
<li onClick={this.props.onClick}>{this.props.task.text}</li>
);
}
}
MyTask.propTypes = {
task: React.PropTypes.object.isRequired,
}
You can pass the event handler callback as a prop to MyTask:
<MyTask onClick={this.handleTaskClick.bind(this)} ... />
And then use it in MyTask:
<li onClick={this.props.onClick}>...</li>
See: https://facebook.github.io/react/docs/tutorial.html#callbacks-as-props
Related
import {createStyles, WithStyles} from "#material-ui/core";
const styles = (theme: Theme) => createStyles({
root: {}
});
interface MyProps extends WithStyles<typeof styles> {
}
export class MyComponent extends Component<MyProps> {
constructor(props: MyProps) {
super(props);
console.log(props.classes); // why this is undefined?
}
}
Why props.classes is undefined?
You can send props to component like where you calling
<MyComponent classes={.. Any thing you want to pass here ...} />
Finally got it working by "decorating" my class like this
export const MyComponent = withStyles(styles)(
class extends Component<MyProps> {
...
}
)
Then you can use the styles like this
<div className={this.props.classes.root}>
I'm trying to navigate between react native screens from my Backend class like this:
var self = this;
firebase.auth().onAuthStateChanged((user) => {
if (user) {
self.setState({
userID: user.uid,
})
} else{
self.props.navigation.navigate("Login");
}
});
My backend class is not a component and therefore is not imported into the stack navigator I am using. I am getting an error saying 'self.props.navigation is not an object'.
Does anyone know I can fix this? Thanks
One not-so-good practice is to define your Navigator as a static/class variable of your App instance:
const MyNavigator = StackNavigator(...);
export default class MyApp extends Component {
render() {
return <MyNavigator ref={(ref) => MyApp.Navigator = ref}/>
}
}
then you can access your navigator and it's props and functions anywhere you want! (for example dispatch a back event):
import MyApp from '...';
MyApp.Navigator.dispatch(NavigationActions.back());
I am personally not a fan of navigation actions happening at that level however, sometimes it's necessary. Expanding on the answer from #Dusk a pattern was made known to me that helps with this very solution. You can find it here
https://github.com/react-community/react-navigation/issues/1439#issuecomment-303661539
The idea is that you create a service that holds a ref to your navigator. Now from anywhere in your app you can import that service and have access to your navigator. It keeps it clean and concise.
If you are using react-navigation then you can achieve this via Navigation Service
Create a file named NavigationService and add the below code there
import { NavigationActions, StackActions } from 'react-navigation';
let navigator;
function setTopLevelNavigator(navigatorRef) {
navigator = navigatorRef;
}
function navigate(routeName, params) {
navigator.dispatch(
NavigationActions.navigate({
routeName,
params
})
);
}
function goBack(routeName, params) {
navigator.dispatch(
StackActions.reset({
index: 0,
actions: [
NavigationActions.navigate({
routeName,
params
})
]
})
);
}
function replace(routeName, params) {
navigator.dispatch(
StackActions.replace({
index: 0,
actions: [
NavigationActions.navigate({
routeName,
params
})
]
})
);
}
function pop() {
navigator.dispatch(StackActions.pop());
}
function popToTop() {
navigator.dispatch(StackActions.popToTop());
}
// add other navigation functions that you need and export them
export default {
navigate,
goBack,
replace,
pop,
popToTop,
setTopLevelNavigator
};
Now import this file in your app.js and set the TopLevelNavigator, your app.js will look something like this
import React, { Component } from 'react';
import NavigationService from './routes/NavigationService';
export default class App extends Component {
constructor() {
super();
}
render() {
return (
<View style={{ flex: 1, backgroundColor: '#fff' }}>
<AppNavigator
ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}}
/>
</View>
);
}
}
Now you are good to go, you can import your NavigationService where ever you want, you can use it like this in any of the components and non-component files
import NavigationService from 'path to the NavigationService file';
/* you can use any screen name you have defined in your StackNavigators
* just replace the LogInScreen with your screen name and it will work like a
* charm
*/
NavigationService.navigate('LogInScreen');
/*
* you can also pass params or extra data into the ongoing screen like this
*/
NavigationService.navigate('LogInScreen',{
orderId: this.state.data.orderId
});
I found the child component Element was not connected when I want to use action in child component.
how to connect it ?
code:
import { connect } from 'react-redux';
import * as actions from '../actions';
class Element extends React.Component {
constructor(props) {
super(props);
}
render() {
let { children } = props.el.children;
return (
<div>
{ children ?
children.map(el =>
<Element key={ 'el-' + el.id } el={ el } />
)
:
null
}
</div>
)
}
}
Element = connect(
null
,
actions
)(Element);
export default Element;
You are rendering <Elemnt /> inside it's own render function.
If you just want to pass the children down the render function of your Element component then just change it to this:
import { connect } from 'react-redux';
import * as actions from '../actions';
class Element extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{ this.props.children }
</div>
)
}
}
Element = connect(
null
,
actions
)(Element);
export default Element;
Note that the wrapper <div> is there because current version of react only allow one top level element in the render body, and this.props.children can hold more than one element.
Very basic simple GET example for react-redux
I have a "MockAPI" which simulates a GET request to an API like so:
const dashboards = [
{
"Id":1,
"title":"Overview"
},
{
"Id":2,
"title":"Overview"
},
{
"Id":3,
"title":"Overview"
},
{
"Id":4,
"title":"Overview"
}
];
class DashboardApi {
static getAllDashboards() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(Object.assign([], dashboards));
}, delay);
});
}
}
I am trying to develop in a react-redux flow of dispatching an action via a button click and then updating the component via the redux store.
Here is my component code:
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import * as dashboardActions from '../../actions/dashboardActions';
class HomePage extends React.Component {
constructor(props) {
super(props);
this.loadDashboards = this.loadDashboards.bind(this);
}
loadDashboards() {
this.props.dispatch(dashboardActions.loadDashboards());
}
dashboardItem(dashboard, index) {
return <p key={index}>{dashboard.title}</p>;
}
render() {
return (
<div>
<h1>
Hello World!
<button onClick={this.loadDashboards}>load</button>
</h1>
{this.props.dashboards.map(this.dashboardItem)}
</div>
);
}
}
HomePage.propTypes = {
dashboards: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired
};
function mapStateToProps(state) {
return {
dashboards: state.dashboards
};
}
export default connect(mapStateToProps)(HomePage);
And here is my dashboardActions.js:
import * as types from './actionTypes';
import dashboardApi from '../mockApi/mockDashboardApi';
export function loadDashboardsSuccess(dashboards) {
return { type: types.LOAD_DASHBOARDS_SUCCESS, dashboards };
}
export function loadDashboards() {
return dispatch => {
return dashboardApi
.getAllDashboards()
.then(dashboards => {
dispatch(loadDashboardsSuccess(dashboards));
});
};
}
And here is my reducer:
import initialState from './initialState';
import * as types from '../actions/actionTypes';
export default function dashboardReducer(state = initialState.dashboards, action) {
switch(action.types) {
case types.LOAD_DASHBOARDS_SUCCESS:
return action.dashboards;
default:
return state;
}
}
I am trying to get the onClick to load in the dashboards array and to render as <p> tags simply displaying the title value. Unfortunately it is not happening.
I see that the LOAD_DASHBOARDS_SUCCESS action is getting loaded, but I see that the dashboards property in the store is still an empty array instead of showing the returned data...
What am I missing here?
You've got a typo in your reducer. switch(action.types) should be switch(action.type) with no 's'
I have the following react component that passed data to another component:
export default class App extends TrackerReact(Component){
getUserFrameData(){
return (FrameCollection.find().fetch());
}
render(){
return(
<div className="main-container">
<Frames
data={this.getUserFrameData()}
/>
</div>
);
}
}
Now I want my frames component to do an action when the component initialises.
export default class Frames extends Component{
componentDidMount(){
console.log(this.props.data);
}
render() {...}
}
But on I only get empty data at on loadup. I think it's because I'm using subscriptions and a login system. So how can I tell my Frames component to wait until everything is "loaded up"?
Use the ready method of the subscription object.
constructor() {
super();
this.state = {
sub: Meteor.subscribe('myPublication')
}
}
render() {
if (!this.state.sub.ready()) return <p>Loading...</p>;
return ...
}
http://docs.meteor.com/api/pubsub.html#Subscription-ready