Invoking functions on nested components in react redux - reactjs

Sorry if this question displays a lack of understanding of the react paradigm which I suspect it does however I am very new to the tech.
I want to dispatch an action from my 'actions' code which should ultimately call a function on a nested component. Up to now I have been simply modifying state by dispatching actions and catching them in the reducer which works fine.
I am working on a portlet as part of a wider framework and I can capture an onExport() message in the actions. From here I have no idea of the best way to call the nested component (I need access to the inner ag-grid in the nested component to export it).
I have considered introducing some new 'exportRequested' state flag and setting this in the reducer then using componentDidReceiveProps in the nested component. I have also been studying the 'connect' idea and this seems right in so far as it would allow me to expose the function and connect it to the store but I can't seem to join the dots and figure out how to invoke it from the reducer. Is there some way to sort of dispatch an action and catch it directly in the nested component?
some code:
Container:
import {initData} from '/actions';
export class MainComponent extends PureComponent {
static propTypes = {
initData: func.isRequired,
data: array.isRequired,
};
static defaultProps = {
data: [],
};
componentDidMount() {
this.props.initData();
}
render() {
const { data } = this.props;
return (
<div>
<ChildGrid data={data} />
</div>
);
}
}
export default connect(
state => ({
data: getData(state),
}),
{ initData }
)(MainComponent);
Nested Grid:
export class ChildGrid extends PureComponent {
static propTypes = {
data: array.isRequired,
};
static defaultProps = {
data: [],
};
exportData() {
// HOW TO MESSAGE THIS FROM ACTIONS. I want to call DataGrid.gridApi.exportAsCsv()
}
render() {
const { data } = this.props;
return (
<div>
<DataGrid
rowData={data}
/>
</div>
);
}
}

You thought of the correct solution to your problem, by creating a state flag in your redux store then listening to the change of that property in your nested component. Unfortunately in Redux we can't listen to specific events or specific state property changes of the Redux store.
The implementation of such a solution is as follows:
ChildGrid.jsx
class ChildGrid extends PureComponent {
static propTypes = {
data: array.isRequired,
};
static defaultProps = {
data: [],
};
componentWillReceiveProps(newProps) {
if (newProps.exportRequested) {
this.exportData();
}
}
exportData() {
DataGrid.gridApi.exportAsCsv();
}
render() {
const { data } = this.props;
return (
<div>
<DataGrid
rowData={data}
/>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
reduxExportRequested: state.exportRequested
};
};
export default connect(mapStateToProps)(ChildGrid);

Related

React this.setState not working with context API

Currently I have this code from my AppContext.js file
import React, { Component, createContext } from 'react';
export const AppContext = createContext();
export class AppProvider extends Component {
state = {
test: '',
};
getEntries() {
console.log('FIRED');
this.setState({test: 'HELLO'});
}
render() {
return (
<AppContext.Provider
value={{
...this.state,
getEntries: this.getEntries
}}
>
{this.props.children}
</AppContext.Provider>
);
}
}
I'm calling the getEntries function and its displaying message from the console successfully but the this.setState is not working it says TypeError: this.setState is not a function
The problem here is this is not bound to the right context.
The simplest workaround is probably to use this syntax:
getEntries = () => {
...
}
There are several ways in React to bind this to the class context: check this article for other ways.
getEntries function needs to be bind to the component. The simple way to do it is to use arrow function as shown below.
getEntries = () => {
console.log('FIRED');
this.setState({test: 'HELLO'});
}
The second way to bind getEnteries method to the component is
constructor(props) {
super(props);
// This binding is necessary to make `this` work in the callback
this.getEntries = this.getEntries.bind(this);
}

HOC pass through properties

I'm lately struggling with complex HOC and how I can pass through only the new props defined in it and not any other.
More precisely, suppose my HOC makes use of other HOCs which extends its properties, for instance
const withSession = (WrappedComponent) => {
class SessionProvider extends React.PureComponent {
constructor(props) {
super(props);
this.login = this.login.bind(this);
}
login() {
console.log('login'); // this will dispatch some action
// this.props.dispatch...
}
render() {
return (
<WrappedComponent
doLogin={this.login}
{...this.props}
/>
);
}
}
const mapStateToProps = null;
function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
return compose(
withConnect,
withRouter,
)(injectIntl(SessionProvider));
};
Here the SessionProvider makes use of dispatch and injectIntl which attach properties to its props. However, I don't want to pass those props down to the wrapped component. The idea is to have a SessionProvider HOC which has some API call but only extends the wrapped component with login.
I noticed that if keep {...this.props}, the wrapped component will also get all the props used by the HOC which I don't want to pass through.
So I thought to explicitly define which properties to pass through by decomposing this.props by changing the HOC render method:
render() {
const { dispatch, intl, ...otherProps } = this.props;
return <WrappedComponent doLogin={this.login} { ...otherProps} />;
}
However what happens with this is that if the WrappedComponent itself has dispach or intl props, those are not passed-through the HOC.
Is there anything wrong in what I'm doing? Any better approach? Am I missing anything?
There's nothing wrong in what you're doing. Prop name conflicts is a known issue when using HOCs. So, as far as I can tell, the best alternative you could use is Render Props pattern, which helps to keep components render as declarative as possible. For your case, consider something like this:
class Session extends React.PureComponent {
constructor(props) {
super(props);
this.login = this.login.bind(this);
}
login() {
console.log("login"); // this will dispatch some action
// this.props.dispatch...
}
// ...
render() {
return (
<React.Fragment>
{this.props.children({
doLogin: this.login
doLogout: this.logout
// ...
})}
</React.Fragment>
);
}
}
// ...
return compose(
withConnect,
withRouter
)(injectIntl(Session));
And use it from another components:
// ...
render() {
return (
<Session>
{({ doLogin, doLogout }) => (
<React.Fragment>
<SomeComponent doLogin={doLogin} />
<button onClick={doLogout}>Logout</button>
</React.Fragment>
)}
</Session>
)
}
UPDATE:
There's a pretty promising Hooks Proposal available in v16.7.0-alpha. I'm not quite familiar with them yet, but they tend to solve components reusability more efficiently.
You need to copy static properties, for that i use below code.. you can add more properties as per your need
export const REACT_STATICS = {
childContextTypes: true,
contextTypes: true,
defaultProps: true,
displayName: true,
getDefaultProps: true,
mixins: true,
propTypes: true,
type: true
};
export const KNOWN_STATICS = {
name: true,
length: true,
prototype: true,
caller: true,
arguments: true,
arity: true
};
export function hoistStatics(targetComponent, sourceComponent) {
var keys = Object.getOwnPropertyNames(sourceComponent);
for (var i = 0; i < keys.length; ++i) {
const key = keys[i];
if (!REACT_STATICS[key] && !KNOWN_STATICS[key]) {
try {
targetComponent[key] = sourceComponent[key];
} catch (error) {}
}
}
return targetComponent;
}
// in HOC
const hoistedSessionProvider = hoistStatics(SessionProvider, WrappedComponent);
// use hoistedSessionProvider in compose

Retrieving a Redux state, but also change another Redux state in same React Native component?

I have a React component that currently just retrieves a state from Redux. Here is the general layout:
const mapStateToProps = state => {
return { stuff: state.stuff };
};
class MyComponent extends React.Component {
// use 'stuff' from redux to build the Views
}
export default connect(mapStateToProps)(MyComponent);
But now, what if I want to add a button that changes another Redux state called other?
To save the new Redux state, I know we have to create a dispatch to the action. ie,
const mapDispatchToProps = dispatch => {
....
};
Then finally connect them:
connect(null, mapDispatchToProps)(MyComponent);
But my confusion is if I am already connecting with mapStateToProps, how can I also map it to mapDispatchToProps so that I can update the Redux state in the same component?
You can use both ;-)
For example :
Action.js
export const textChanged = (newText) => {
return { type: "TEXT_CHANGED", newText }
};
HomeScene.js :
import { textChanged } from "../actions;
...
render () {
const { myText } = this.props;
<TextInput
value={myText}
onChangeText={(newText) => this.props.textChanged(newText)}
/>
}
function mapStateToProps(state) {
return {
myText: state.appContent.myText
};
}
export default connect(mapStateToProps, { textChanged })(HomeScene);
Reducer.js
case "TEXT_CHANGED":
return {
...state,
myText: action.newText
};
Hope it helps !
Hm, looks like I asked too early. I did a bit of reading and the parameters in connect() actually accepts both.
So like this:
connect(mapStateToProps, mapDispatchToProps)(MyComponent)

How can I store and subscribe component state to redux store?

I have a container "HomeIndexView" and component "Table"
And I have both global and local component state of table.
Global table states are like below,
const initialState = {
allTables: [],
showForm: false,
fetching: true,
formErrors: null,
};
and local component state of table is like below,
componentWillMount() {
this.setInitialState();
}
setInitialState() {
this.setState({ tableBusy: false });
}
When a user logs in, in HomeIndexView, it shows all tables from data base through fetching.
So what I want to do is that connecting local component state to redux store so that when it changes state false to true, it changes background color of table. How should I connect local state to redux store? and should I create separate reducer and action for the local component's state?
Thanks in advance
--EDIT 1
import Table from '../../components/tables/table';
I am importing LocalComponent (Table) to HomeIndexView to show.
In my HomeIndexView, it renders all tables from database,
_renderAllTables() {
const { fetching } = this.props;
let content = false;
if(!fetching) {
content = (
<div className="tables-wrapper">
{::this._renderTables(this.props.tables.allTables)}
</div>
);
}
return (
<section>
<header className="view-header">
<h3>All Tables</h3>
</header>
{content}
</section>
);
}
_renderTables(tables) {
return tables.map((table) => {
return <Table
key={table.id}
dispatch={this.props.dispatch}
{...table} />;
});
}
render() {
return (
<div className="view-container tables index">
{::this._renderAllTables()}
</div>
);
}
The 'react-redux' library contains binding methods between React and Redux. If you haven't done so already, I really recommend checking out
Dan Abramov's: 'Getting into Redux' series of videos.
He goes into a good amount of detail about how to build a working Redux application from scratch and then how to do the same in conjunction with React (again from scratch).
He finalises on the use of the 'react-redux' helper library to make wiring up React with Redux easier.
The resulting solution for you would be to:
Use the connect method in Redux to create a Redux container component (just a term for a React component with Redux bindings)
mapStateToProps receives updates on the current state of the store which you can map to the target components props. Yo'd use this to get the current state of the store for use in your component
mapDispatchToProps which gets the store's dispatch action which you can use to bind action creators to (to update the store). You'd use this to connect the action creators that update the state of your store.
I assume you've already setup your reducers and actions. Now the only thing you need to do is dispatch an action from your LocalComponent.
Lets say you've a method called toggleTableState for updating the state tableBusy of your LocalComponent.
LocalComponent.js
import React, { PureComponent, PropTypes } from 'react';
import { connect } from 'react-redux';
import * as actions from './actions`;
#connect(
(state, props) => ({
isTableBusy: state.isTableBusy,
}),
{
toggleTableGlobalState: actions.toggleTableGlobalState,
})
export default class LocalComponent extends PureComponent {
static propTypes = {
toggleTableGlobalState: PropTypes.func,
isTableBusy: PropTypes.bool,
}
...
toggleTableState() {
this.setState({ tableBusy: !this.state.tableBusy });
this.props.toggleTableGlobalState(!this.state.tableBusy);
}
...
}
actions.js
export const TOGGLE_TABLE_STATE = 'TOGGLE_TABLE_STATE';
export function toggleTableGlobalState(tableState) {
return { type: TOGGLE_TABLE_STATE, tableState };
}
reducer.js
import { TOGGLE_TABLE_STATE } from './actions';
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
...
case TOGGLE_TABLE_STATE:
return { ...state, isTableBusy: action.tableState };
break;
...
}
}

Correct place to load data in connected redux component

Where is the correct place to load data in a redux component?
Currently I have it this way.
Say I have this container component:
import { loadResultsPage } from '../actions/winratio-actions';
function mapStateToProps(state) {
const {
isFetching,
results
} = state;
return {
isFetching,
results
};
};
export default connect(mapStateToProps, {
loadResultsPage
})(WinRatio);
I then make a call in the wrapped component's componentWillMount lifecycle event:
export default class WinRatio extends Component {
componentWillMount() {
this.props.loadResultsPage();
}
render() {
return (
<div>
<h1>Win Ratio</h1>
</div>
);
}
};
Where should this call happen?
You can do it in your container component. If you use redux you probably use smart\dumb components strategy. Create a container where you use compose function from redux package and you can compose it like this:
export default compose(
connect(null, { loadData }), //this is your async action
doOnComponentMount(({props}) => props.loadData()),
)(MyDumbComponent)
and doOnComponentMount is:
function doOnComponentMount(cb) {
return (Component) => {
return class extends React.Component {
componentDidMount() {
cb(this);
}
render() {
return <Component {...this.props} />;
}
}
}
}

Resources