Building a Twitter Clone App in React Native
Getting your hands on new programming languages or frameworks is one of the greatest methods to learn them.
Therefore, we made the decision to build a Twitter clone app while learning React Native app development. In order to demonstrate how to build this app, we will first quickly outline our experience with React Native before getting into the code.
Our Experience with React Native
1. Code Excellence
When using React Native app development services, brute forcing a solution will leave you with a tonne of source code and little understanding of how it functions. If you fix one item, another will break.
Yes, it’s not very encouraging, but on the good side, you’ll have to keep the code quality at a good scale.
2. Ease of Development
A basic understanding of Javascript is required, else, at first glance React Native can appear to be complex science. But be assured, it isn’t. React Native app development framework stands apart from other Javascript frameworks in major part because it doesn’t introduce a tonne of abstraction levels. It keeps you rather near to JavaScript’s fundamentals.
So, if you are comfortable with all fundamental DOM and JavaScript ideas, React Native is for you!
After working on a few projects, you’ll see that React Native gives you a lot of freedom without significantly increasing the complexity of your work.
3. Performance
React Native compiles JavaScript code into a fully native app, in contrast to previous mobile technologies that offer you a “mobile app” that is actually running in webview.
React Native was found to perform better in a recent performance comparison between an app created using Swift and an identical app created with Swift.
4. Community Support
For their projects, several well-known businesses, start-ups, and independent developers use React Native. The React Native community has grown dramatically over the last several years.
It gives you the support to create something helpful and enhance an open-source project created by another mobile app developer.
Building a Twitter Clone App in React Native
1. Getting Started
Node.js, yarn, or NPM must all be installed. We used yarn and node v8.1.2 for our project.
A new React Native project can be started in a variety of methods; for this one, we used create-react-native-app. Run the following commands to launch a new project and instal CRNA globally:
1 2 3 4 5 6 7 |
$ yarn global add create-react-native-app $ create-react-native-app twitter-clone $ cd twitter-clone/ $ yarn start |
Additionally, we have utilized the Expo SDK to avoid having to build both Xcode and Android Studio. Additionally, this will give me access to tools that will facilitate the loading of typefaces and the ability for users to add photographs to the app. Please consult the documentation to get Expo up and running.
2. Dependencies
We’ll discuss a few of the most important tools we’ll be using for this project below.
- With the help of the Native Base component library, we can easily create a cross-platform user interface that looks great.
- We may move between the various displays of our app with the aid of React Native Router Flux.
- React-Redux will link the various parts of our program to our store, where we’ll maintain information about its current state.
- We will use Axios, a promised-based HTTP client, to perform our requests to the Cosmic JS API.
Look at the code below you can make use of your package.json and then run yarn install again.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
{ "name": "twitter-clone", "version": "1.0.0", "private": true, "devDependencies": { "jest-expo": "~1.0.1", "react-native-scripts": "0.0.30", "react-test-renderer": "16.0.0-alpha.6" }, "main": "./node_modules/react-native-scripts/build/bin/crna-entry.js", "scripts": { "start": ""react-native-scripts start", "eject": "react-native-scripts eject", "android": "react-native-scripts android", "ios": "react-native-scripts ios", "test": "node node_modules/jest/bin/jest.js --watch" }, "jest": { "preset": "jest-expo" }, "dependencies": { "@expo/vector-icons": "^5.0.0", "axios": "^0.16.1", "expo": "^17.0.0", "form-data": "^2.2.0", "native-base": "^2.1.4", "react": "16.0.0-alpha.6", "react-native": "^0.44.0", "react-native-router-flux": "^3.39.2", "react-redux": "^5.0.5", "redux": "^3.6.0", "redux-devtools-extension": "^2.13.2", "redux-logger": "^3.0.6", "redux-thunk": "^2.2.0", } } |
3. Directory Structure
CRNA is far less prescriptive about how we organize the files in our application than some other boilerplates; it just provides us with index.ios.js, index.android.js, and App.js as a starting point.
Our App.js file will refer to the app folder, which will include all of our components, layouts, configuration files, redux store, and reducers. The framework that we have discovered to be most effective for us is as follows. The source code has a complete list of the contents of every file, so let’s not go into it right now.
Here is how our application folder will appear:
4. Application
Several events will take place in our App.js code. We’ll do:
- Draw in the paths that lead to our varied layouts.
- Connect our provider to our store so that our layouts may access the status of our application.
- Give users access to some of the typefaces used by Native Base
- With AppRegistry, establish our root component.
The following may be copied and pasted into your project root’s App.js file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
import React, { Component } from 'react'; import { AppRegistry, View } from 'react-native'; import { Provider, connect } from 'react-redux'; import { Font, AppLoading } from 'expo'; import store from './app/redux/store'; import Router from './app/config/routes'; export default class App extends Component { constructor(){ super(); this.state = { isReady: false, } } async componentWillMount() { await Font.loadAsync({ 'Roboto': require('native-base/Fonts/Roboto.ttf'), 'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'), 'Pacifico': require('./app/assets/fonts/Pacifico.ttf'), 'Ionicons': require('native-base/Fonts/Ionicons.ttf'), }); this.setState({isReady: true}); } render() { if (!this.state.isReady) { return <AppLoading />; } return ( <Provider store={store}> <Router /> </Provider> ); } } AppRegistry.registerComponent('main', () => App); |
Next, let’s take a look at our routes.js file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import React from 'react'; import { Scene, Router, Actions, ActionConst } from 'react-native-router-flux'; import Welcome from '../layouts/welcome'; import Login from '../layouts/login'; import Signup from '../layouts/signup'; import NewPost from '../layouts/newPost'; import Feed from '../layouts/feed'; const scenes = Actions.create( <Scene key="root"> <Scene key="welcome" component={Welcome} title="Welcome" initial={true} /> <Scene key="login" component={Login} title="Login" type={ActionConst.REPLACE} /> <Scene key="signup" component={Signup} title="Create New Account" type={ActionConst.REPLACE} /> <Scene key="feed" component={Feed} title="Your Feed" type={ActionConst.REPLACE} hideNavBar /> <Scene key="newPost" component={NewPost} title="Make a new post" /> </Scene> ); export default () => ( <Router scenes={scenes} /> ); |
We’ve just built a number of scenes that are accessible from wherever in our app using React Native Router Flux.
The Welcome layout is the opening scene, where users can choose to log in or register a new account. It appears as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
import React from 'react'; import { Container, Content, Icon, Text, Button, } from 'native-base'; import { View } from 'react-native'; import { Actions } from 'react-native-router-flux'; import styles from './styles'; export default () => ( <Container style={styles.container}> <Content> <View style={styles.iconBox}> <Icon style={styles.icon} ios="ios-happy-outline" android="md-happy" /> <Text style={styles.welcome}>Welcome</Text> </View> <View style={styles.buttonContainer}> <Button block style={styles.button} onPress={() => Actions.login()} > <Text>Log in</Text> </Button> <Text style={styles.or}>OR</Text> <Button block style={styles.button} onPress={() => Actions.signup()} > <Text>Sign up</Text> </Button> </View> </Content> </Container> ) |
Here, we’ve only used Native Base to construct two buttons that will direct users to the Login and Signup layouts.
See what occurs when people create a new account by taking a peek at our Signup layout.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
import React, { Component } from 'react'; import { connect } from 'react-redux'; import { ImagePicker } from 'expo'; import { Actions } from 'react-native-router-flux'; import {View} from 'react-native'; import { Container, Content, Button, Text, Form, Thumbnail, Icon } from 'native-base'; import axios from 'axios'; import TextField from '../../components/TextField'; import styles from './styles'; import { addUser } from '../../redux/reducers/users'; import cosmicConfig from '../../config/cosmic'; const mapDispatchToProps = {addUser}; const validate = form => { let errorMessage = ''; if (form.username.includes(" ")){ errorMessage = "Username cannot contain spaces"; } if (form.password.includes(" ")){ errorMessage = "Password cannot contain spaces"; } Object.keys(form).slice(0, 5).map(field => { if (!form[field]){ errorMessage = 'All fields must be filled'; } }) return errorMessage; } class Signup extends Component { constructor() { super(); this.state = { firstName: '', lastName: '', username: '', password: '', image: null, error: '', }; } onSubmit(){ const error = validate(this.state); if (error) { this.setState({ error }) } else { this.checkUsername(this.state.username); } } checkUsername(username){ axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/users/search?metafield_key=username&metafield_value=${username}`) .then(res => res.data) .then(data => { if (data.objects) { this.setState({ error: 'Username not available'}) } else { this.props.addUser(this.state); } }) } uploadImage = async () => { let result = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, aspect: [4, 3], }); if (!result.cancelled) { this.setState({ image: result.uri }); } }; render(){ return ( <Container style={styles.container}> <Content> <Form style={styles.mar10}> <TextField name="First Name" value={this.state.firstName} onChangeText={(text) => this.setState({firstName: text})} /> <TextField name="Last Name" value={this.state.lastName} onChangeText={(text) => this.setState({lastName: text})} /> <TextField name="Username" value={this.state.username} onChangeText={(text) => this.setState({username: text})} /> <TextField secureTextEntry name="Password" value={this.state.password} onChangeText={(text) => this.setState({password: text})} /> </Form> <Text style={styles.addPic}>Add a profile picture</Text> { !this.state.image && <Button primary bordered onPress={this.uploadImage} style={styles.uploadButton}> <Icon ios='ios-camera' android='md-camera' /> </Button> } { this.state.image && <Thumbnail size={80} source={{uri: this.state.image}} style={styles.thumbnail} /> } <Button block style={styles.mar10} onPress={() => this.onSubmit()} > <Text>Create account</Text> </Button> <Text style={styles.formMsg}>{this.state.error}</Text> <Button transparent style={styles.loginBtn} onPress={() => Actions.login()} > <Text style={styles.loginTxt}>Already have an account?</Text> </Button> </Content> </Container> ); } } export default connect(null, mapDispatchToProps)(Signup); |
Several events might happen in this situation:
- As users fill out the form, the contents of the fields are kept in state.
- We perform some basic validation when users submit to make sure they have entered proper data into the fields.
- In order to confirm that the username they have chosen is not already in use, we then make our first call to the Cosmic JS API.
- Finally, we utilize our addUserfunction to submit the form as a new user to the Cosmic JS API after all of the fields have valid input.
Our usersreducer contains a definition for the addUser method that looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
export const addUser = user => dispatch => { let data = new FormData(); data.append('media', { uri: user.image, type: 'image/jpeg', name: 'image' }); return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/media`, data) .then(res => res.data.media) .then(media => { return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/add-object`, { title: user.firstName + ' ' + user.lastName, type_slug: 'users', metafields: [ { key: 'name', type: 'text', value: user.firstName + ' ' + user.lastName, }, { key: 'username', type: 'text', value: user.username, }, { key: 'password', type: 'text', value: user.password, }, { key: 'profile_picture', type: 'file', value: media.name, } ] } )} ) .then(res => formatUser(res.data)) .then(formattedUser => dispatch(createUser(formattedUser))) .then(() => Actions.feed()) .catch(err => console.error(`Creating user unsuccessful`, err)) } |
Here, we use the Cosmic.js API for more than one time. The user’s profile photo will be added to our bucket as media in response to the first request, and the user’s complete information will be added as a new user in reaction to the second call using a reference to the picture we receive back.
The user can log in if they have previously registered an account by:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Container, Content, Icon, Text, Button, } from 'native-base'; import { View } from 'react-native'; import { Actions } from 'react-native-router-flux'; import TextField from '../../components/TextField'; import styles from './styles'; import { authenticate } from '../../redux/reducers/users'; const mapDispatchToProps = {authenticate}; const validate = form => { let errorMessage = ''; if (form.username.includes(' ') || form.password.includes(' ')){ errorMessage = 'Username and password cannot contain spaces'; } if (form.username === '' || form.password === ''){ errorMessage = 'All fields must be filled'; } return errorMessage; } class Login extends Component { constructor(props) { super(props); this.state = { username: '', password: '', error: '', }; } onSubmit(){ const error = validate(this.state); if (error) { this.setState({ error }) } else { this.login(); } } login(){ this.props.authenticate(this.state) .then(res => { if (res === 'Username invalid' || res === 'Password invalid'){ this.setState({ error: res, username: '', password: '', }) } else { Actions.feed(); } }); } render(){ return ( <Container style={styles.container}> <Content> <Text style={styles.formMsg}>{this.state.error}</Text> <Icon style={styles.icon} ios="ios-happy-outline" android="md-happy" /> <View style={styles.loginBox}> <TextField name="Enter Username" type="big" value={this.state.username} onChangeText={(text) => this.setState({username: text})} /> <TextField secureTextEntry name="Enter Password" type="big" value={this.state.password} onChangeText={(text) => this.setState({password: text})} /> <Button block style={styles.button} onPress={() => this.onSubmit()} > <Text>Log in</Text> </Button> </View> <Button transparent style={styles.signupBtn} onPress={() => Actions.signup()}> <Text style={styles.signupTxt}>Sign up for an account</Text> </Button> </Content> </Container> ); } } export default connect(null, mapDispatchToProps)(Login); |
Once more, we verify that the fields contain correct input before using our authenticatefunction to compare the login information to what is in our bucket:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
export const authenticate = user => dispatch => { return axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/users/search?metafield_key=username&metafield_value=${user.username}`) .then(res => res.data) .then(data => { console.log('RESPONSE: ', data); if (data.objects) { const userData = data.objects[0]; return { password: userData.metadata.password, username: userData.metadata.username, name: userData.metadata.name, profilePicture: userData.metadata.profile_picture, slug: userData.slug, id: userData._id, } } else { return 'Username invalid'; } }) .then(data => { if (data === 'Username invalid'){ return data; } else if (data.password === user.password){ dispatch(login({ name: data.name, username: data.username, profilePicture: data.profilePicture, slug: data.slug, id: data.id, })) } else { return 'Password invalid'; } }) .catch(error => console.error('Login unsuccessful', error)) } |
As a side note, we shouldn’t ordinarily store user credentials in our database directly without using encryption, but we’ll keep it that way for the time being as a quick example of how we may handle our data using the Cosmic API.
After logging in, users are sent immediately to the Feed layout, which is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Actions } from 'react-native-router-flux'; import { Container, Content, List, Button, Icon, Text, } from 'native-base'; import SinglePost from '../../components/SinglePost'; import FeedNavbar from '../../components/FeedNavbar'; import { loadPosts } from '../../redux/reducers/posts'; import { logoutUser } from '../../redux/reducers/users'; import styles from './styles'; const mapStateToProps = ({ posts }) => ({ posts }); const mapDispatchToProps = { loadPosts, logoutUser }; const renderPost = (post, index) => ( <SinglePost key={index} name={post.name} username={post.username} profilePicture={post.profilePicture} content={post.content} /> ) class Feed extends Component { componentDidMount(){ this.props.loadPosts(); } render(){ const endMsg = this.props.posts.length === 0 ? "There aren't any posts yet!" : "That's all the posts for now!" return ( <Container> <FeedNavbar logout={this.props.logoutUser} refresh={this.props.loadPosts} /> <Content> <List> { !!this.props.posts.length && this.props.posts.map(renderPost) } </List> <Text style={styles.end}>{endMsg}</Text> </Content> <Button rounded style={styles.button} onPress={() => Actions.newPost()} > <Icon name="create" style={{padding: 5}} /> </Button> </Container> ); } } export default connect(mapStateToProps, mapDispatchToProps)(Feed); |
We send a request to the Cosmic API when the Feed layout mounts in order to load all of the posts in our bucket into our app state. In our posts reduction, the loadPostsfunction looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export const loadPosts = () => dispatch => { return axios.get(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/object-type/posts`) .then(res => res.data.objects ? formatPosts(res.data.objects) : []) .then(formattedPosts => formattedPosts.sort(postSorter)) .then(sortedPosts => dispatch(init(sortedPosts))) .catch(err => console.error(`Could not load posts`, err)); }; |
We extract all of the posts from our bucket, prepare them so we can easily access the information we need, and then put them into the state. They are then shown in the stream.
Users may create new posts by clicking a button on the feed. After that, they are sent to the NewPostlayout:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
import React, { Component } from 'react'; import { connect } from 'react-redux'; import { Container, Content, Text, Button, } from 'native-base'; import { View } from 'react-native'; import TextField from '../../components/TextField'; import styles from './styles'; import { createPost } from '../../redux/reducers/posts'; const mapStateToProps = state => ({ user: state.user, }) const mapDispatchToProps = { createPost }; class NewPost extends Component { constructor(){ super(); this.state = { content: '', error: '', } } onSubmit() { if (this.state.content){ this.props.createPost({ user: this.props.user, content: this.state.content, }) } else { this.setState({error: 'You have to write something!'}); } } render(){ return ( <Container style={styles.container}> <Content> <Text style={styles.formMsg}>{this.state.error}</Text> <View style={styles.input}> <TextField big name="What's up?" value={this.state.post} onChangeText={(text) => this.setState({content: text})} /> <Button rounded style={styles.button} onPress={() => this.onSubmit()} > <Text>Post</Text> </Button> </View> </Content> </Container> ); } } export default connect(mapStateToProps, mapDispatchToProps)(NewPost); |
We’ll add their post to our bucket as soon as they submit it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
return axios.post(`https://api.cosmicjs.com/v1/${cosmicConfig.bucket.slug}/add-object`, { title: post.user.username + ' post', type_slug: 'posts', content: post.content, metafields: [ { type: 'object', title: 'User', key: 'user', object_type: 'users', value: post.user.id }, ] }) .then(res => formatPost(res.data, post)) .then(formattedPost => dispatch(create(formattedPost))) .then(() => Actions.feed({type: 'popAndReplace'})) .catch(error => console.error('Post unsuccessful', error)) } |
And now we’ll reroute to the feed once more, bringing up the most recent collection of posts. Users can log out of the Feed and refresh their feed to see any new entries.
Hurray! Our Twitter clone app using React Native app development framework is ready to launch!
Conclusion
We used React Native to create a Twitter-like app that made use of the Cosmic JS API to make it simple to manage all of the user and post data. With a few straightforward operations that POST and Be our data to/from our Cosmic JS Bucket, we were able to get up and running quickly.
It may not be easy to create a Twitter-like app alone, therefore, you need an experienced React Native app development company to help you match your app ideation. Need to hire react native developer? Your search ends here! Get in touch with us to get all you have been looking for in one platform itself.